From d8206c0e3ecc20f06d5179b0601f6d952d78549b Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Wed, 5 Oct 2022 15:34:52 +0200 Subject: [PATCH] wip: docker compose --- .../migration.sql | 3 + .../migration.sql | 2 + apps/api/prisma/schema.prisma | 77 +- apps/api/src/jobs/deployApplication.ts | 251 +++--- apps/api/src/lib/buildPacks/common.ts | 1 + apps/api/src/lib/buildPacks/compose.ts | 107 ++- .../routes/api/v1/applications/handlers.ts | 13 +- .../src/routes/api/v1/applications/types.ts | 5 +- apps/ui/package.json | 1 + apps/ui/src/lib/templates.ts | 13 +- .../[id]/configuration/_BuildPack.svelte | 12 +- .../[id]/configuration/buildpack.svelte | 98 ++- .../src/routes/applications/[id]/index.svelte | 754 ++++++++++-------- docker-compose-dev.yaml | 4 + package.json | 4 +- pnpm-lock.yaml | 2 + 16 files changed, 831 insertions(+), 516 deletions(-) create mode 100644 apps/api/prisma/migrations/20221005120323_initial_docker_compose/migration.sql create mode 100644 apps/api/prisma/migrations/20221005132352_docker_compose_configuration/migration.sql diff --git a/apps/api/prisma/migrations/20221005120323_initial_docker_compose/migration.sql b/apps/api/prisma/migrations/20221005120323_initial_docker_compose/migration.sql new file mode 100644 index 000000000..bb93e1aaf --- /dev/null +++ b/apps/api/prisma/migrations/20221005120323_initial_docker_compose/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "Application" ADD COLUMN "dockerComposeFile" TEXT; +ALTER TABLE "Application" ADD COLUMN "dockerComposeFileLocation" TEXT; diff --git a/apps/api/prisma/migrations/20221005132352_docker_compose_configuration/migration.sql b/apps/api/prisma/migrations/20221005132352_docker_compose_configuration/migration.sql new file mode 100644 index 000000000..e7368dc1a --- /dev/null +++ b/apps/api/prisma/migrations/20221005132352_docker_compose_configuration/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Application" ADD COLUMN "dockerComposeConfiguration" TEXT; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index eba0ac215..d782bceae 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -94,43 +94,46 @@ model TeamInvitation { } model Application { - id String @id @default(cuid()) - name String - fqdn String? - repository String? - configHash String? - branch String? - buildPack String? - projectId Int? - port Int? - exposePort Int? - installCommand String? - buildCommand String? - startCommand String? - baseDirectory String? - publishDirectory String? - deploymentType String? - phpModules String? - pythonWSGI String? - pythonModule String? - pythonVariable String? - dockerFileLocation String? - denoMainFile String? - denoOptions String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - destinationDockerId String? - gitSourceId String? - baseImage String? - baseBuildImage String? - gitSource GitSource? @relation(fields: [gitSourceId], references: [id]) - destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id]) - persistentStorage ApplicationPersistentStorage[] - settings ApplicationSettings? - secrets Secret[] - teams Team[] - connectedDatabase ApplicationConnectedDatabase? - previewApplication PreviewApplication[] + id String @id @default(cuid()) + name String + fqdn String? + repository String? + configHash String? + branch String? + buildPack String? + projectId Int? + port Int? + exposePort Int? + installCommand String? + buildCommand String? + startCommand String? + baseDirectory String? + publishDirectory String? + deploymentType String? + phpModules String? + pythonWSGI String? + pythonModule String? + pythonVariable String? + dockerFileLocation String? + denoMainFile String? + denoOptions String? + dockerComposeFile String? + dockerComposeFileLocation String? + dockerComposeConfiguration String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + destinationDockerId String? + gitSourceId String? + baseImage String? + baseBuildImage String? + gitSource GitSource? @relation(fields: [gitSourceId], references: [id]) + destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id]) + persistentStorage ApplicationPersistentStorage[] + settings ApplicationSettings? + secrets Secret[] + teams Team[] + connectedDatabase ApplicationConnectedDatabase? + previewApplication PreviewApplication[] } model PreviewApplication { diff --git a/apps/api/src/jobs/deployApplication.ts b/apps/api/src/jobs/deployApplication.ts index a2790aadb..f25c4bbc3 100644 --- a/apps/api/src/jobs/deployApplication.ts +++ b/apps/api/src/jobs/deployApplication.ts @@ -212,17 +212,37 @@ import * as buildpacks from '../lib/buildPacks'; // } 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 (!imageFound || deployNeeded) { - // if (true) { if (buildpacks[buildPack]) await buildpacks[buildPack]({ dockerId: destinationDocker.id, + network: destinationDocker.network, buildId, applicationId, domain, name, type, + volumes, + labels, pullmergeRequestId, buildPack, repository, @@ -248,7 +268,7 @@ import * as buildpacks from '../lib/buildPacks'; denoOptions, baseImage, baseBuildImage, - deploymentType + deploymentType, }); else { await saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId }); @@ -257,112 +277,137 @@ import * as buildpacks from '../lib/buildPacks'; } else { await saveBuildLog({ line: 'Build image already available - no rebuild required.', buildId, applicationId }); } - try { - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker stop -t 0 ${imageId}` }) - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker rm ${imageId}` }) - } catch (error) { - // - } - const envs = [ - `PORT=${port}` - ]; - if (secrets.length > 0) { - secrets.forEach((secret) => { - if (pullmergeRequestId) { - const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) - if (isSecretFound.length > 0) { - envs.push(`${secret.name}=${isSecretFound[0].value}`); - } else { - envs.push(`${secret.name}=${secret.value}`); - } - } else { - if (!secret.isPRMRSecret) { - envs.push(`${secret.name}=${secret.value}`); - } + + if (buildPack === 'compose') { + try { + await executeDockerCmd({ + dockerId: destinationDockerId, + command: `docker ps -a --filter 'label=coolify.applicationId=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0` + }) + await executeDockerCmd({ + dockerId: destinationDockerId, + command: `docker ps -a --filter 'label=coolify.applicationId=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker rm --force` + }) + } catch (error) { + // + } + try { + await executeDockerCmd({ debug, buildId, applicationId, dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` }) + await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId }); + await saveBuildLog({ line: 'Proxy will be updated shortly.', 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' + } + }); } - }); - } - await fs.writeFile(`${workdir}/.env`, envs.join('\n')); - const labels = makeLabelForStandaloneApplication({ - applicationId, - fqdn, - name, - type, - pullmergeRequestId, - buildPack, - repository, - branch, - projectId, - port: exposePort ? `${exposePort}:${port}` : port, - commit, - installCommand, - buildCommand, - startCommand, - baseDirectory, - publishDirectory - }); - let envFound = false; - try { - envFound = !!(await fs.stat(`${workdir}/.env`)); - } catch (error) { - // - } - try { - await saveBuildLog({ line: 'Deployment started.', buildId, applicationId }); - const composeVolumes = volumes.map((volume) => { - return { - [`${volume.split(':')[0]}`]: { - name: volume.split(':')[0] - } - }; - }); - const composeFile = { - version: '3.8', - services: { - [imageId]: { - image: `${applicationId}:${tag}`, - container_name: imageId, - volumes, - env_file: envFound ? [`${workdir}/.env`] : [], - labels, - depends_on: [], - expose: [port], - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - // logging: { - // driver: 'fluentd', - // }, - ...defaultComposeConfiguration(destinationDocker.network), - } - }, - networks: { - [destinationDocker.network]: { - external: true - } - }, - volumes: Object.assign({}, ...composeVolumes) - }; - await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile)); - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` }) - await saveBuildLog({ line: 'Deployment successful!', 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); + } + + } else { + try { + await executeDockerCmd({ + dockerId: destinationDockerId, + command: `docker ps -a --filter 'label=com.docker.compose.service=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0` + }) + await executeDockerCmd({ + dockerId: destinationDockerId, + command: `docker ps -a --filter 'label=com.docker.compose.service=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker rm --force` + }) + } catch (error) { + // + } + const envs = [ + `PORT=${port}` + ]; + if (secrets.length > 0) { + secrets.forEach((secret) => { + if (pullmergeRequestId) { + const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) + if (isSecretFound.length > 0) { + envs.push(`${secret.name}=${isSecretFound[0].value}`); + } else { + envs.push(`${secret.name}=${secret.value}`); + } + } else { + if (!secret.isPRMRSecret) { + envs.push(`${secret.name}=${secret.value}`); + } } }); } - throw new Error(error); + await fs.writeFile(`${workdir}/.env`, envs.join('\n')); + + let envFound = false; + try { + envFound = !!(await fs.stat(`${workdir}/.env`)); + } catch (error) { + // + } + try { + await saveBuildLog({ line: 'Deployment started.', buildId, applicationId }); + const composeVolumes = volumes.map((volume) => { + return { + [`${volume.split(':')[0]}`]: { + name: volume.split(':')[0] + } + }; + }); + const composeFile = { + version: '3.8', + services: { + [imageId]: { + image: `${applicationId}:${tag}`, + container_name: imageId, + volumes, + env_file: envFound ? [`${workdir}/.env`] : [], + 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 executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` }) + await saveBuildLog({ line: 'Deployment successful!', 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); + } + await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId }); + await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } }); + if (!pullmergeRequestId) await prisma.application.update({ + where: { id: applicationId }, + data: { configHash: currentHash } + }); } - await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId }); - await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } }); - if (!pullmergeRequestId) await prisma.application.update({ - where: { id: applicationId }, - data: { configHash: currentHash } - }); } } catch (error) { diff --git a/apps/api/src/lib/buildPacks/common.ts b/apps/api/src/lib/buildPacks/common.ts index 98a387f2f..5fe971cc6 100644 --- a/apps/api/src/lib/buildPacks/common.ts +++ b/apps/api/src/lib/buildPacks/common.ts @@ -634,6 +634,7 @@ export function makeLabelForStandaloneApplication({ return [ 'coolify.managed=true', `coolify.version=${version}`, + `coolify.applicationId=${applicationId}`, `coolify.type=standalone-application`, `coolify.configuration=${base64Encode( JSON.stringify({ diff --git a/apps/api/src/lib/buildPacks/compose.ts b/apps/api/src/lib/buildPacks/compose.ts index 30af1f10b..71cb72b21 100644 --- a/apps/api/src/lib/buildPacks/compose.ts +++ b/apps/api/src/lib/buildPacks/compose.ts @@ -1,34 +1,99 @@ import { promises as fs } from 'fs'; -import { executeDockerCmd } from '../common'; -import { buildImage } from './common'; +import { defaultComposeConfiguration, executeDockerCmd } from '../common'; +import { buildImage, saveBuildLog } from './common'; import yaml from 'js-yaml'; +import { getSecrets } from '../../routes/api/v1/applications/handlers'; export default async function (data) { - let { - applicationId, + let { + applicationId, + debug, + buildId, dockerId, - debug, - tag, - workdir, - buildId, - baseDirectory, - secrets, - pullmergeRequestId, - dockerFileLocation - } = data - const file = `${workdir}${baseDirectory}/docker-compose.yml`; - const dockerComposeRaw = await fs.readFile(`${file}`, 'utf8') + network, + volumes, + labels, + workdir, + baseDirectory, + secrets, + pullmergeRequestId, + port + } = data + const fileYml = `${workdir}${baseDirectory}/docker-compose.yml`; + const fileYaml = `${workdir}${baseDirectory}/docker-compose.yaml`; + let dockerComposeRaw = null; + let isYml = false; + try { + dockerComposeRaw = await fs.readFile(`${fileYml}`, 'utf8') + isYml = true + } catch (error) { } + try { + dockerComposeRaw = await fs.readFile(`${fileYaml}`, 'utf8') + } catch (error) { } + + if (!dockerComposeRaw) { + throw ('docker-compose.yml or docker-compose.yaml are not found!'); + } const dockerComposeYaml = yaml.load(dockerComposeRaw) if (!dockerComposeYaml.services) { throw 'No Services found in docker-compose file.' } + const envs = [ + `PORT=${port}` + ]; + if (getSecrets.length > 0) { + secrets.forEach((secret) => { + if (secret.isBuildSecret) { + if (pullmergeRequestId) { + const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) + if (isSecretFound.length > 0) { + envs.push(`${secret.name}=${isSecretFound[0].value}`); + } else { + envs.push(`${secret.name}=${secret.value}`); + } + } else { + if (!secret.isPRMRSecret) { + envs.push(`${secret.name}=${secret.value}`); + } + } + } + }); + } + await fs.writeFile(`${workdir}/.env`, envs.join('\n')); + let envFound = false; + try { + envFound = !!(await fs.stat(`${workdir}/.env`)); + } catch (error) { + // + } + const composeVolumes = volumes.map((volume) => { + return { + [`${volume.split(':')[0]}`]: { + name: volume.split(':')[0] + } + }; + }); + let networks = {} for (let [key, value] of Object.entries(dockerComposeYaml.services)) { value['container_name'] = `${applicationId}-${key}` - console.log({key, value}); + value['env_file'] = envFound ? [`${workdir}/.env`] : [] + value['labels'] = labels + value['volumes'] = volumes + 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 } } - - throw 'Halting' - // await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker compose --project-directory ${workdir} pull` }) - // await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker compose --project-directory ${workdir} build --progress plain --pull` }) - // await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker compose --project-directory ${workdir} up -d` }) + dockerComposeYaml['volumes'] = Object.assign({ ...dockerComposeYaml['volumes'] }, ...composeVolumes) + dockerComposeYaml['networks'] = Object.assign({ ...networks }, { [network]: { external: true } }) + await fs.writeFile(`${workdir}/docker-compose.${isYml ? 'yml' : 'yaml'}`, yaml.dump(dockerComposeYaml)); + await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker compose --project-directory ${workdir} pull` }) + await saveBuildLog({ line: 'Pulling images from Compose file.', buildId, applicationId }); + await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker compose --project-directory ${workdir} build --progress plain` }) + await saveBuildLog({ line: 'Building images from Compose file.', buildId, applicationId }); } diff --git a/apps/api/src/routes/api/v1/applications/handlers.ts b/apps/api/src/routes/api/v1/applications/handlers.ts index be06a3519..65b0750b2 100644 --- a/apps/api/src/routes/api/v1/applications/handlers.ts +++ b/apps/api/src/routes/api/v1/applications/handlers.ts @@ -289,13 +289,16 @@ export async function saveApplication(request: FastifyRequest, baseImage, baseBuildImage, deploymentType, - baseDatabaseBranch + baseDatabaseBranch, + dockerComposeFile, + dockerComposeFileLocation, + dockerComposeConfiguration } = request.body + console.log({dockerComposeConfiguration}) if (port) port = Number(port); if (exposePort) { exposePort = Number(exposePort); } - const { destinationDocker: { engine, remoteEngine, remoteIpAddress }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } }) if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress }) if (denoOptions) denoOptions = denoOptions.trim(); @@ -324,6 +327,9 @@ export async function saveApplication(request: FastifyRequest, baseImage, baseBuildImage, deploymentType, + dockerComposeFile, + dockerComposeFileLocation, + dockerComposeConfiguration, ...defaultConfiguration, connectedDatabase: { update: { hostedDatabaseDBName: baseDatabaseBranch } } } @@ -342,6 +348,9 @@ export async function saveApplication(request: FastifyRequest, baseImage, baseBuildImage, deploymentType, + dockerComposeFile, + dockerComposeFileLocation, + dockerComposeConfiguration, ...defaultConfiguration } }); diff --git a/apps/api/src/routes/api/v1/applications/types.ts b/apps/api/src/routes/api/v1/applications/types.ts index 443deb00f..14ba30b78 100644 --- a/apps/api/src/routes/api/v1/applications/types.ts +++ b/apps/api/src/routes/api/v1/applications/types.ts @@ -21,7 +21,10 @@ export interface SaveApplication extends OnlyId { baseImage: string, baseBuildImage: string, deploymentType: string, - baseDatabaseBranch: string + baseDatabaseBranch: string, + dockerComposeFile: string, + dockerComposeFileLocation: string, + dockerComposeConfiguration: string } } export interface SaveApplicationSettings extends OnlyId { diff --git a/apps/ui/package.json b/apps/ui/package.json index 805539a5d..fdf37b8aa 100644 --- a/apps/ui/package.json +++ b/apps/ui/package.json @@ -48,6 +48,7 @@ "daisyui": "2.24.2", "dayjs": "1.11.5", "js-cookie": "3.0.1", + "js-yaml": "4.1.0", "p-limit": "4.0.0", "svelte-file-dropzone": "^1.0.0", "svelte-select": "4.4.7", diff --git a/apps/ui/src/lib/templates.ts b/apps/ui/src/lib/templates.ts index 5219879e5..d71fd9323 100644 --- a/apps/ui/src/lib/templates.ts +++ b/apps/ui/src/lib/templates.ts @@ -29,7 +29,7 @@ export function findBuildPack(pack: string, packageManager = 'npm') { port: 80 }; } - if (pack === 'docker') { + if (pack === 'docker' || pack === 'compose') { return { ...metaData, installCommand: null, @@ -39,6 +39,7 @@ export function findBuildPack(pack: string, packageManager = 'npm') { port: null }; } + if (pack === 'svelte') { return { ...metaData, @@ -236,13 +237,13 @@ export const buildPacks = [ isCoolifyBuildPack: true, }, { - name: 'compose', + name: 'compose', type: 'base', fancyName: 'Docker Compose', hoverColor: 'hover:bg-sky-700', color: 'bg-sky-700', isCoolifyBuildPack: true, - }, + }, { name: 'svelte', type: 'specific', @@ -357,14 +358,14 @@ export const buildPacks = [ color: 'bg-green-700', isCoolifyBuildPack: true, }, - { - name: 'heroku', + { + name: 'heroku', type: 'base', fancyName: 'Heroku', hoverColor: 'hover:bg-purple-700', color: 'bg-purple-700', isHerokuBuildPack: true, - } + } ]; export const scanningTemplates = { '@sveltejs/kit': { diff --git a/apps/ui/src/routes/applications/[id]/configuration/_BuildPack.svelte b/apps/ui/src/routes/applications/[id]/configuration/_BuildPack.svelte index 13db5a24a..f7771e026 100644 --- a/apps/ui/src/routes/applications/[id]/configuration/_BuildPack.svelte +++ b/apps/ui/src/routes/applications/[id]/configuration/_BuildPack.svelte @@ -14,6 +14,8 @@ export let foundConfig: any; export let scanning: any; export let packageManager: any; + export let dockerComposeFile: any = null; + export let dockerComposeFileLocation: string | null = null; async function handleSubmit(name: string) { try { @@ -25,10 +27,12 @@ delete tempBuildPack.fancyName; delete tempBuildPack.color; delete tempBuildPack.hoverColor; - - if (foundConfig?.buildPack !== name) { - await post(`/applications/${id}`, { ...tempBuildPack, buildPack: name }); - } + await post(`/applications/${id}`, { + ...tempBuildPack, + buildPack: name, + dockerComposeFile, + dockerComposeFileLocation + }); await post(`/applications/${id}/configuration/buildpack`, { buildPack: name }); return await goto(from || `/applications/${id}`); } catch (error) { diff --git a/apps/ui/src/routes/applications/[id]/configuration/buildpack.svelte b/apps/ui/src/routes/applications/[id]/configuration/buildpack.svelte index 3a4c41115..ad9b70be4 100644 --- a/apps/ui/src/routes/applications/[id]/configuration/buildpack.svelte +++ b/apps/ui/src/routes/applications/[id]/configuration/buildpack.svelte @@ -34,12 +34,15 @@ import { buildPacks, findBuildPack, scanningTemplates } from '$lib/templates'; import { errorNotification } from '$lib/common'; import BuildPack from './_BuildPack.svelte'; + import yaml from 'js-yaml'; const { id } = $page.params; - let scanning = true; + let scanning: boolean = true; let foundConfig: any = null; - let packageManager = 'npm'; + let packageManager: string = 'npm'; + let dockerComposeFile: any = null; + let dockerComposeFileLocation: string | null = null; export let apiUrl: any; export let projectId: any; @@ -60,10 +63,14 @@ } } } - async function scanRepository(): Promise { + async function scanRepository(isPublicRepository: boolean): Promise { try { if (type === 'gitlab') { - const files = await get(`${apiUrl}/v4/projects/${projectId}/repository/tree`, { + if (isPublicRepository) { + return; + } + const url = isPublicRepository ? `` : `/v4/projects/${projectId}/repository/tree`; + const files = await get(`${apiUrl}${url}`, { Authorization: `Bearer ${$appSession.tokens.gitlab}` }); const packageJson = files.find( @@ -82,6 +89,14 @@ (file: { name: string; type: string }) => file.name === 'Dockerfile' && file.type === 'blob' ); + const dockerComposeFileYml = files.find( + (file: { name: string; type: string }) => + file.name === 'docker-compose.yml' && file.type === 'blob' + ); + const dockerComposeFileYaml = files.find( + (file: { name: string; type: string }) => + file.name === 'docker-compose.yaml' && file.type === 'blob' + ); const cargoToml = files.find( (file: { name: string; type: string }) => file.name === 'Cargo.toml' && file.type === 'blob' @@ -105,11 +120,12 @@ const laravel = files.find( (file: { name: string; type: string }) => file.name === 'artisan' && file.type === 'blob' ); - if (yarnLock) packageManager = 'yarn'; if (pnpmLock) packageManager = 'pnpm'; - if (dockerfile) { + if (dockerComposeFileYml || dockerComposeFileYaml) { + foundConfig = findBuildPack('dockercompose', packageManager); + } else if (dockerfile) { foundConfig = findBuildPack('docker', packageManager); } else if (packageJson && !laravel) { const path = packageJson.path; @@ -135,8 +151,13 @@ foundConfig = findBuildPack('node', packageManager); } } else if (type === 'github') { + const headers = isPublicRepository + ? {} + : { + Authorization: `token ${$appSession.tokens.github}` + }; const files = await get(`${apiUrl}/repos/${repository}/contents?ref=${branch}`, { - Authorization: `Bearer ${$appSession.tokens.github}`, + ...headers, Accept: 'application/vnd.github.v2.json' }); const packageJson = files.find( @@ -155,6 +176,14 @@ (file: { name: string; type: string }) => file.name === 'Dockerfile' && file.type === 'file' ); + const dockerComposeFileYml = files.find( + (file: { name: string; type: string }) => + file.name === 'docker-compose.yml' && file.type === 'file' + ); + const dockerComposeFileYaml = files.find( + (file: { name: string; type: string }) => + file.name === 'docker-compose.yaml' && file.type === 'file' + ); const cargoToml = files.find( (file: { name: string; type: string }) => file.name === 'Cargo.toml' && file.type === 'file' @@ -182,7 +211,25 @@ if (yarnLock) packageManager = 'yarn'; if (pnpmLock) packageManager = 'pnpm'; - if (dockerfile) { + if (dockerComposeFileYml || dockerComposeFileYaml) { + foundConfig = findBuildPack('compose', packageManager); + const data = await get( + `${apiUrl}/repos/${repository}/contents/${ + dockerComposeFileYml ? 'docker-compose.yml' : 'docker-compose.yaml' + }?ref=${branch}`, + { + ...headers, + Accept: 'application/vnd.github.v2.json' + } + ); + if (data?.content) { + const content = atob(data.content); + dockerComposeFile = JSON.stringify(yaml.load(content) || null); + dockerComposeFileLocation = dockerComposeFileYml + ? 'docker-compose.yml' + : 'docker-compose.yaml'; + } + } else if (dockerfile) { foundConfig = findBuildPack('docker', packageManager); } else if (packageJson && !laravel) { const data: any = await get(`${packageJson.git_url}`, { @@ -237,7 +284,7 @@ if (error.message === 'Bad credentials') { const { token } = await get(`/applications/${id}/configuration/githubToken`); $appSession.tokens.github = token; - return await scanRepository(); + return await scanRepository(isPublicRepository); } return errorNotification(error); } finally { @@ -246,11 +293,7 @@ } } onMount(async () => { - if (!isPublicRepository) { - await scanRepository(); - } else { - scanning = false; - } + await scanRepository(isPublicRepository); }); @@ -266,7 +309,12 @@
{#each buildPacks.filter((bp) => bp.isHerokuBuildPack === true) as buildPack}
- +
{/each}
@@ -274,9 +322,16 @@
Coolify Base
- {#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type ==='base') as buildPack} + {#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type === 'base') as buildPack}
- +
{/each}
@@ -284,9 +339,14 @@
Coolify Specific
- {#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type ==='specific') as buildPack} + {#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type === 'specific') as buildPack}
- +
{/each}
diff --git a/apps/ui/src/routes/applications/[id]/index.svelte b/apps/ui/src/routes/applications/[id]/index.svelte index 5f09c7db6..c5ff925f0 100644 --- a/apps/ui/src/routes/applications/[id]/index.svelte +++ b/apps/ui/src/routes/applications/[id]/index.svelte @@ -29,7 +29,7 @@ export let application: any; export let settings: any; import { page } from '$app/stores'; - import { onDestroy, onMount } from 'svelte'; + import { onMount } from 'svelte'; import Select from 'svelte-select'; import { get, post } from '$lib/api'; import cuid from 'cuid'; @@ -45,10 +45,9 @@ import { t } from '$lib/translations'; import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common'; import Setting from '$lib/components/Setting.svelte'; - import Tooltip from '$lib/components/Tooltip.svelte'; import Explainer from '$lib/components/Explainer.svelte'; import { goto } from '$app/navigation'; - import { fade } from 'svelte/transition'; + import yaml from 'js-yaml'; const { id } = $page.params; @@ -58,6 +57,10 @@ let loading = false; let fqdnEl: any = null; let forceSave = false; + let isPublicRepository = application.settings.isPublicRepository; + let apiUrl = application.gitSource.apiUrl; + let branch = application.branch; + let repository = application.repository; let debug = application.settings.debug; let previews = application.settings.previews; let dualCerts = application.settings.dualCerts; @@ -66,6 +69,11 @@ let isBot = application.settings.isBot; let isDBBranching = application.settings.isDBBranching; + let dockerComposeFile = JSON.parse(application.dockerComposeFile) || null; + let dockerComposeServices: any[] = []; + let dockerComposeFileLocation = application.dockerComposeFileLocation; + let dockerComposeConfiguration = JSON.parse(application.dockerComposeConfiguration) || {}; + let baseDatabaseBranch: any = application?.connectedDatabase?.hostedDatabaseDBName || null; let nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, ''); let isHttps = application.fqdn && application.fqdn.startsWith('https://'); @@ -86,6 +94,26 @@ label: 'Uvicorn' } ]; + + function normalizeDockerServices(services: any[]) { + const tempdockerComposeServices = []; + for (const [name, data] of Object.entries(services)) { + tempdockerComposeServices.push({ + name, + data + }); + } + for (const service of tempdockerComposeServices) { + if (!dockerComposeConfiguration[service.name]) { + dockerComposeConfiguration[service.name] = {}; + } + } + return tempdockerComposeServices; + } + if (dockerComposeFile?.services) { + dockerComposeServices = normalizeDockerServices(dockerComposeFile.services); + } + function containerClass() { return 'text-white bg-transparent font-thin px-0 w-full border border-dashed border-coolgray-200'; } @@ -214,7 +242,11 @@ dualCerts, exposePort: application.exposePort })); - await post(`/applications/${id}`, { ...application, baseDatabaseBranch }); + await post(`/applications/${id}`, { + ...application, + baseDatabaseBranch, + dockerComposeConfiguration: JSON.stringify(dockerComposeConfiguration) + }); setLocation(application, settings); $isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application); @@ -281,6 +313,36 @@ return false; } } + async function reloadCompose() { + try { + const headers = isPublicRepository + ? {} + : { + Authorization: `token ${$appSession.tokens.github}` + }; + const data = await get( + `${apiUrl}/repos/${repository}/contents/${dockerComposeFileLocation}?ref=${branch}`, + { + ...headers, + Accept: 'application/vnd.github.v2.json' + } + ); + if (data?.content) { + const content = atob(data.content); + let dockerComposeFileContent = JSON.stringify(yaml.load(content) || null); + let dockerComposeFileContentJSON = JSON.parse(dockerComposeFileContent); + dockerComposeServices = normalizeDockerServices(dockerComposeFileContentJSON?.services); + application.dockerComposeFile = dockerComposeFileContent; + await handleSubmit(); + } + addToast({ + message: 'Compose file reloaded.', + type: 'success' + }); + } catch (error) { + errorNotification(error); + } + }
@@ -372,18 +434,20 @@ />
-
- changeSettings('isBot')} - title="Is your application a bot?" - description="You can deploy applications without domains or make them to listen on the Exposed Port.

Useful to host Twitch bots, regular jobs, or anything that does not require an incoming HTTP connection." - disabled={$status.application.isRunning} - /> -
- {#if !isBot} + {#if application.buildPack !== 'compose'} +
+ changeSettings('isBot')} + title="Is your application a bot?" + description="You can deploy applications without domains or make them to listen on the Exposed Port.

Useful to host Twitch bots, regular jobs, or anything that does not require an incoming HTTP connection." + disabled={$status.application.isRunning} + /> +
+ {/if} + {#if !isBot && application.buildPack !== 'compose'}
- {#if isHttps} + {#if isHttps && application.buildPack !== 'compose'}
- -
Build & Deploy
-
- {#if application.buildCommand || application.buildPack === 'rust' || application.buildPack === 'laravel'} -
- -
- -
-
- {/if} - {#if application.buildPack !== 'docker' && (application.buildPack === 'nextjs' || application.buildPack === 'nuxtjs')} -
- -
- +
- {#if isDBBranching} - + - {#if application.connectedDatabase} -
- - -
-
- Connected to {application.connectedDatabase.databaseId} -
+
+ +
+
+ {/if} + {#if $features.beta} + {#if !application.settings.isBot && !application.settings.isPublicRepository} +
+ changeSettings('isDBBranching')} + title="Enable DB Branching" + description="Enable DB Branching" + /> +
+ {#if isDBBranching} + + {#if application.connectedDatabase} +
+ + +
+
+ Connected to {application.connectedDatabase.databaseId} +
+ {/if} {/if} {/if} {/if} - {/if} - {#if application.buildPack === 'python'} -
- -
- -
- {#if application.pythonWSGI?.toLowerCase() === 'gunicorn'} + {#if application.buildPack === 'python'}
- + +
+
+ {#if application.pythonWSGI?.toLowerCase() === 'gunicorn'} +
+ + +
+ {/if} + {#if application.pythonWSGI?.toLowerCase() === 'uvicorn'} +
+ + +
+ {/if} {/if} - {#if application.pythonWSGI?.toLowerCase() === 'uvicorn'} -
- + {#if !staticDeployments.includes(application.buildPack)} +
+
{/if} - {/if} - {#if !staticDeployments.includes(application.buildPack)} -
- - -
- {/if} -
- - -
- {#if !notNodeDeployments.includes(application.buildPack)} -
- - -
-
- - -
-
- - -
- {/if} - {#if application.buildPack === 'deno'} -
- - -
-
-
+ {/if}
diff --git a/docker-compose-dev.yaml b/docker-compose-dev.yaml index 468d17efc..c9588c7a8 100644 --- a/docker-compose-dev.yaml +++ b/docker-compose-dev.yaml @@ -22,6 +22,10 @@ services: published: 3001 protocol: tcp mode: host + - target: 5555 + published: 5555 + protocol: tcp + mode: host volumes: - ./:/app - '/var/run/docker.sock:/var/run/docker.sock' diff --git a/package.json b/package.json index de44428ea..e692b61f6 100644 --- a/package.json +++ b/package.json @@ -8,14 +8,16 @@ "oc": "opencollective-setup", "translate": "pnpm run --filter i18n-converter translate", "db:studio": "pnpm run --filter api db:studio", + "db:studio:container": "docker exec coolify pnpm run --filter api db:studio", "db:push": "pnpm run --filter api db:push", "db:seed": "pnpm run --filter api db:seed", "db:migrate": "pnpm run --filter api db:migrate", + "db:migrate:container": "docker exec coolify pnpm run --filter api db:migrate", "format": "run-p -l -n format:*", "format:api": "NODE_ENV=development pnpm run --filter api format", "lint": "run-p -l -n lint:*", "lint:api": "NODE_ENV=development pnpm run --filter api lint", - "dev:container": "docker-compose -f docker-compose-dev.yaml up", + "dev:container": "docker-compose -f docker-compose-dev.yaml up || docker compose -f docker-compose-dev.yaml up", "dev": "run-p -l -n dev:api dev:ui", "dev:api": "NODE_ENV=development pnpm run --filter api dev", "dev:ui": "NODE_ENV=development pnpm run --filter ui dev", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0820847ab..bac8b5276 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -159,6 +159,7 @@ importers: flowbite: 1.5.2 flowbite-svelte: 0.26.2 js-cookie: 3.0.1 + js-yaml: 4.1.0 p-limit: 4.0.0 postcss: 8.4.16 prettier: 2.7.1 @@ -181,6 +182,7 @@ importers: daisyui: 2.24.2_25hquoklqeoqwmt7fwvvcyxm5e dayjs: 1.11.5 js-cookie: 3.0.1 + js-yaml: 4.1.0 p-limit: 4.0.0 svelte-file-dropzone: 1.0.0 svelte-select: 4.4.7