diff --git a/apps/api/prisma/migrations/20221201115801_simple_dockerfile_deployment/migration.sql b/apps/api/prisma/migrations/20221201115801_simple_dockerfile_deployment/migration.sql new file mode 100644 index 000000000..b3406e59e --- /dev/null +++ b/apps/api/prisma/migrations/20221201115801_simple_dockerfile_deployment/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Application" ADD COLUMN "simpleDockerfile" TEXT; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index f3adfe4aa..098d18e50 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -98,7 +98,7 @@ model TeamInvitation { } model Application { - id String @id @default(cuid()) + id String @id @default(cuid()) name String fqdn String? repository String? @@ -124,23 +124,25 @@ model Application { dockerComposeFile String? dockerComposeFileLocation String? dockerComposeConfiguration String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt destinationDockerId String? gitSourceId String? gitCommitHash 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[] dockerRegistryId String? - dockerRegistry DockerRegistry? @relation(fields: [dockerRegistryId], references: [id]) + simpleDockerfile String? + + persistentStorage ApplicationPersistentStorage[] + secrets Secret[] + teams Team[] + connectedDatabase ApplicationConnectedDatabase? + previewApplication PreviewApplication[] + gitSource GitSource? @relation(fields: [gitSourceId], references: [id]) + destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id]) + dockerRegistry DockerRegistry? @relation(fields: [dockerRegistryId], references: [id]) } model PreviewApplication { diff --git a/apps/api/src/jobs/deployApplication.ts b/apps/api/src/jobs/deployApplication.ts index b8f23b6fc..2ae04b3aa 100644 --- a/apps/api/src/jobs/deployApplication.ts +++ b/apps/api/src/jobs/deployApplication.ts @@ -3,7 +3,7 @@ import crypto from 'crypto'; import fs from 'fs/promises'; import yaml from 'js-yaml'; -import { copyBaseConfigurationFiles, makeLabelForStandaloneApplication, saveBuildLog, setDefaultConfiguration } from '../lib/buildPacks/common'; +import { copyBaseConfigurationFiles, makeLabelForSimpleDockerfile, makeLabelForStandaloneApplication, saveBuildLog, setDefaultConfiguration } from '../lib/buildPacks/common'; import { createDirectories, decrypt, defaultComposeConfiguration, executeDockerCmd, getDomain, prisma, decryptApplication, isDev } from '../lib/common'; import * as importers from '../lib/importers'; import * as buildpacks from '../lib/buildPacks'; @@ -39,10 +39,155 @@ import * as buildpacks from '../lib/buildPacks'; actions.push(async () => { let application = await prisma.application.findUnique({ where: { id: queueBuild.applicationId }, include: { destinationDocker: true, gitSource: { include: { githubApp: true, gitlabApp: true } }, persistentStorage: true, secrets: true, settings: true, teams: true } }) - let { id: buildId, type, sourceBranch = null, pullmergeRequestId = null, previewApplicationId = null, forceRebuild, sourceRepository = null } = queueBuild - + 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 + } = 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 { + 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}`); + } + } + }); + } + await fs.writeFile(`${workdir}/.env`, envs.join('\n')); + + let envFound = false; + try { + envFound = !!(await fs.stat(`${workdir}/.env`)); + } catch (error) { + // + } + + await fs.writeFile(`${workdir}/Dockerfile`, simpleDockerfile); + const labels = makeLabelForSimpleDockerfile({ + applicationId, + type, + port: exposePort ? `${exposePort}:${port}` : port, + }); + try { + await saveBuildLog({ line: 'Deployment initiated', buildId, applicationId }); + 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, + env_file: envFound ? [`${workdir}/.env`] : [], + 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: 'Deployed successfully 🎉', 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 prisma.build.update({ where: { id: buildId }, data: { status: 'success' } }); + } + } 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 }); + } + } finally { + if (!isDev) { + await fs.rm(workdir, { recursive: true, force: true }); + } + } + return; + } + + const originalApplicationId = application.id const { id: applicationId, @@ -415,8 +560,7 @@ import * as buildpacks from '../lib/buildPacks'; }); } } - } - catch (error) { + } catch (error) { const foundBuild = await prisma.build.findUnique({ where: { id: buildId } }) if (foundBuild) { await prisma.build.update({ diff --git a/apps/api/src/lib/buildPacks/common.ts b/apps/api/src/lib/buildPacks/common.ts index 99e6ea140..0f8f8f36e 100644 --- a/apps/api/src/lib/buildPacks/common.ts +++ b/apps/api/src/lib/buildPacks/common.ts @@ -675,7 +675,14 @@ export async function buildImage({ await saveBuildLog({ line: `Building production image built successful 🎉`, buildId, applicationId }); } } - +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, diff --git a/apps/api/src/lib/common.ts b/apps/api/src/lib/common.ts index 03bf7265e..520802cc3 100644 --- a/apps/api/src/lib/common.ts +++ b/apps/api/src/lib/common.ts @@ -1091,7 +1091,7 @@ export const createDirectories = async ({ repository: string; buildId: string; }): Promise<{ workdir: string; repodir: string }> => { - repository = repository.replaceAll(' ', '') + if (repository) repository = repository.replaceAll(' ', '') const repodir = `/tmp/build-sources/${repository}/`; const workdir = `/tmp/build-sources/${repository}/${buildId}`; let workdirFound = false; diff --git a/apps/api/src/lib/docker.ts b/apps/api/src/lib/docker.ts index a4d36b7fc..c826bd4e7 100644 --- a/apps/api/src/lib/docker.ts +++ b/apps/api/src/lib/docker.ts @@ -41,14 +41,13 @@ export async function checkContainer({ dockerId, container, remove = false }: { `docker rm ${container}` }); } - + return { found: containerFound, status: { isRunning, isRestarting, isExited - } }; } catch (err) { diff --git a/apps/api/src/routes/api/v1/applications/handlers.ts b/apps/api/src/routes/api/v1/applications/handlers.ts index 99e5a8616..aa3b74ab5 100644 --- a/apps/api/src/routes/api/v1/applications/handlers.ts +++ b/apps/api/src/routes/api/v1/applications/handlers.ts @@ -334,7 +334,8 @@ export async function saveApplication(request: FastifyRequest, baseDatabaseBranch, dockerComposeFile, dockerComposeFileLocation, - dockerComposeConfiguration + dockerComposeConfiguration, + simpleDockerfile } = request.body if (port) port = Number(port); if (exposePort) { @@ -373,6 +374,7 @@ export async function saveApplication(request: FastifyRequest, dockerComposeFile, dockerComposeFileLocation, dockerComposeConfiguration, + simpleDockerfile, ...defaultConfiguration, connectedDatabase: { update: { hostedDatabaseDBName: baseDatabaseBranch } } } @@ -395,6 +397,7 @@ export async function saveApplication(request: FastifyRequest, dockerComposeFile, dockerComposeFileLocation, dockerComposeConfiguration, + simpleDockerfile, ...defaultConfiguration } }); @@ -770,22 +773,37 @@ export async function deployApplication(request: FastifyRequest, reply: FastifyReply) { try { const { id } = request.params - const { gitSourceId, forPublic, type } = request.body + const { gitSourceId, forPublic, type, simpleDockerfile } = request.body if (forPublic) { - const publicGit = await prisma.gitSource.findFirst({ where: { type, forPublic } }); + if (gitSourceId) { + await prisma.application.update({ + where: { id }, + data: { gitSource: { connect: { id: gitSourceId } } } + }); + } else { + const publicGit = await prisma.gitSource.findFirst({ where: { type, forPublic } }); + await prisma.application.update({ + where: { id }, + data: { gitSource: { connect: { id: publicGit.id } } } + }); + } + } + if (simpleDockerfile) { await prisma.application.update({ where: { id }, - data: { gitSource: { connect: { id: publicGit.id } } } - }); - } else { - await prisma.application.update({ - where: { id }, - data: { gitSource: { connect: { id: gitSourceId } } } + data: { simpleDockerfile } }); } + return reply.code(201).send() } catch ({ status, message }) { return errorHandler({ status, message }) @@ -916,11 +943,11 @@ export async function getBuildPack(request) { const teamId = request.user?.teamId; const application: any = await getApplicationFromDB(id, teamId); return { - type: application.gitSource.type, + type: application.gitSource?.type || 'dockerRegistry', projectId: application.projectId, repository: application.repository, branch: application.branch, - apiUrl: application.gitSource.apiUrl, + apiUrl: application.gitSource?.apiUrl || null, isPublicRepository: application.settings.isPublicRepository } } catch ({ status, message }) { diff --git a/apps/api/src/routes/api/v1/applications/types.ts b/apps/api/src/routes/api/v1/applications/types.ts index 28ec6725e..adac42ed6 100644 --- a/apps/api/src/routes/api/v1/applications/types.ts +++ b/apps/api/src/routes/api/v1/applications/types.ts @@ -25,7 +25,8 @@ export interface SaveApplication extends OnlyId { baseDatabaseBranch: string, dockerComposeFile: string, dockerComposeFileLocation: string, - dockerComposeConfiguration: string + dockerComposeConfiguration: string, + simpleDockerfile: string } } export interface SaveApplicationSettings extends OnlyId { @@ -56,7 +57,7 @@ export interface GetImages { Body: { buildPack: string, deploymentType: string } } export interface SaveApplicationSource extends OnlyId { - Body: { gitSourceId?: string | null, forPublic?: boolean, type?: string } + Body: { gitSourceId?: string | null, forPublic?: boolean, type?: string, simpleDockerfile?: string } } export interface CheckRepository extends OnlyId { Querystring: { repository: string, branch: string } diff --git a/apps/api/src/routes/api/v1/handlers.ts b/apps/api/src/routes/api/v1/handlers.ts index b7b3ba55a..649e3f124 100644 --- a/apps/api/src/routes/api/v1/handlers.ts +++ b/apps/api/src/routes/api/v1/handlers.ts @@ -190,7 +190,7 @@ export async function showDashboard(request: FastifyRequest) { let foundUnconfiguredApplication = false; for (const application of applications) { - if (!application.buildPack || !application.destinationDockerId || !application.branch || (!application.settings?.isBot && !application?.fqdn) && application.buildPack !== "compose") { + if (((!application.buildPack || !application.branch) && !application.simpleDockerfile) || !application.destinationDockerId || (!application.settings?.isBot && !application?.fqdn) && application.buildPack !== "compose") { foundUnconfiguredApplication = true } } diff --git a/apps/ui/src/lib/components/svg/applications/ApplicationIcons.svelte b/apps/ui/src/lib/components/svg/applications/ApplicationIcons.svelte index fc5dbe16c..387653c87 100644 --- a/apps/ui/src/lib/components/svg/applications/ApplicationIcons.svelte +++ b/apps/ui/src/lib/components/svg/applications/ApplicationIcons.svelte @@ -42,4 +42,6 @@ {:else if application.buildPack?.toLowerCase() === 'compose'} +{:else if application.simpleDockerfile} + {/if} diff --git a/apps/ui/src/lib/container/status.ts b/apps/ui/src/lib/container/status.ts index 466d3569f..a700e652b 100644 --- a/apps/ui/src/lib/container/status.ts +++ b/apps/ui/src/lib/container/status.ts @@ -19,14 +19,14 @@ export async function refreshStatus(list: Array) { } export async function getStatus(resource: any, force: boolean = false) { - const { id, buildPack, dualCerts, engine } = resource; + const { id, buildPack, dualCerts, engine, simpleDockerfile } = resource; let newStatus = 'stopped'; // Already set and we're not forcing if (getStore(containerStatus)[id] && !force) return getStore(containerStatus)[id]; try { - if (buildPack) { // Application + if (buildPack || simpleDockerfile) { // Application const response = await get(`/applications/${id}/status`); newStatus = parseApplicationsResponse(response); } else if (typeof dualCerts !== 'undefined') { // Service diff --git a/apps/ui/src/lib/store.ts b/apps/ui/src/lib/store.ts index ed5b386b9..f72232c21 100644 --- a/apps/ui/src/lib/store.ts +++ b/apps/ui/src/lib/store.ts @@ -57,14 +57,15 @@ export const appSession: Writable = writable({ export const disabledButton: Writable = writable(false); export const isDeploymentEnabled: Writable = writable(false); export function checkIfDeploymentEnabledApplications(isAdmin: boolean, application: any) { - return ( + return !!( isAdmin && (application.buildPack === 'compose') || (application.fqdn || application.settings.isBot) && - application.gitSource && + ((application.gitSource && application.repository && - application.destinationDocker && - application.buildPack + application.buildPack) || application.simpleDockerfile) && + application.destinationDocker + ); } export function checkIfDeploymentEnabledServices(isAdmin: boolean, service: any) { diff --git a/apps/ui/src/routes/applications/[id]/_Menu.svelte b/apps/ui/src/routes/applications/[id]/_Menu.svelte index 702c09911..39a60b29b 100644 --- a/apps/ui/src/routes/applications/[id]/_Menu.svelte +++ b/apps/ui/src/routes/applications/[id]/_Menu.svelte @@ -218,28 +218,30 @@ -
  • - - - - - - - Revert -
  • + + + + + + + Revert + + {/if}
  • Monitoring
  • - {#if !application.settings.isBot} + {#if !application.settings.isBot && !application.simpleDockerfile}
  • {#if !filteredSources}
    Git App
    - {/if} -
    - {#if !filteredSources} +
    {$t('application.configuration.no_configurable_git')} @@ -95,7 +100,13 @@
    - {:else} +
    + {/if} + {#if ownSources.length > 0 || otherSources.length > 0} +
    Integrated with Git App
    + {/if} + {#if ownSources.length > 0} +
    {#each ownSources as source}
    @@ -240,11 +251,25 @@
    {/each}
    - {/if} -
    +
    + {/if}
    -
    Public Repository
    +
    Public Repository from Git
    +
    +
    Simple Dockerfile
    + +
    +
    +
    +
    +
    +