feat: Ability to change deployment type for nextjs

This commit is contained in:
Andras Bacsai 2022-07-08 16:51:58 +02:00
parent c478c1b7ad
commit 88a62be30c
13 changed files with 163 additions and 48 deletions

3
.gitignore vendored
View File

@ -9,4 +9,5 @@ package
dist dist
client client
apps/api/db/*.db apps/api/db/*.db
local-serve local-serve
apps/api/db/migration.db-journal

View File

@ -6,6 +6,7 @@
"db:push": "prisma db push && prisma generate", "db:push": "prisma db push && prisma generate",
"db:seed": "prisma db seed", "db:seed": "prisma db seed",
"db:studio": "prisma studio", "db:studio": "prisma studio",
"db:migrate": "COOLIFY_DATABASE_URL=file:../db/migration.db prisma migrate dev --skip-seed --name",
"dev": "nodemon", "dev": "nodemon",
"build": "rimraf build && esbuild `find src \\( -name '*.ts' \\)| grep -v client/` --platform=node --outdir=build --format=cjs", "build": "rimraf build && esbuild `find src \\( -name '*.ts' \\)| grep -v client/` --platform=node --outdir=build --format=cjs",
"format": "prettier --write 'src/**/*.{js,ts,json,md}'", "format": "prettier --write 'src/**/*.{js,ts,json,md}'",

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Application" ADD COLUMN "deploymentType" TEXT;

View File

@ -91,6 +91,7 @@ model Application {
startCommand String? startCommand String?
baseDirectory String? baseDirectory String?
publishDirectory String? publishDirectory String?
deploymentType String?
phpModules String? phpModules String?
pythonWSGI String? pythonWSGI String?
pythonModule String? pythonModule String?

View File

@ -55,7 +55,8 @@ import * as buildpacks from '../lib/buildPacks';
denoOptions, denoOptions,
exposePort, exposePort,
baseImage, baseImage,
baseBuildImage baseBuildImage,
deploymentType
} = message } = message
let { let {
branch, branch,
@ -225,7 +226,8 @@ import * as buildpacks from '../lib/buildPacks';
denoMainFile, denoMainFile,
denoOptions, denoOptions,
baseImage, baseImage,
baseBuildImage baseBuildImage,
deploymentType
}); });
else { else {
await saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId }); await saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId });

View File

@ -17,7 +17,7 @@ const nodeBased = [
'nextjs' 'nextjs'
]; ];
export function setDefaultBaseImage(buildPack: string | null) { export function setDefaultBaseImage(buildPack: string | null, deploymentType: string | null) {
const nodeVersions = [ const nodeVersions = [
{ {
value: 'node:lts', value: 'node:lts',
@ -259,10 +259,17 @@ export function setDefaultBaseImage(buildPack: string | null) {
baseBuildImages: [] baseBuildImages: []
}; };
if (nodeBased.includes(buildPack)) { if (nodeBased.includes(buildPack)) {
payload.baseImage = 'node:lts'; if (deploymentType === 'static') {
payload.baseImages = nodeVersions; payload.baseImage = 'webdevops/nginx:alpine';
payload.baseBuildImage = 'node:lts'; payload.baseImages = staticVersions;
payload.baseBuildImages = nodeVersions; 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)) { if (staticApps.includes(buildPack)) {
payload.baseImage = 'webdevops/nginx:alpine'; payload.baseImage = 'webdevops/nginx:alpine';
@ -431,7 +438,7 @@ export async function copyBaseConfigurationFiles(
buildId, buildId,
applicationId applicationId
}); });
} else if (staticApps.includes(buildPack) && baseImage.includes('nginx')) { } else if (baseImage.includes('nginx')) {
await fs.writeFile( await fs.writeFile(
`${workdir}/nginx.conf`, `${workdir}/nginx.conf`,
`user nginx; `user nginx;

View File

@ -1,17 +1,22 @@
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { buildImage, checkPnpm } from './common'; import { buildCacheImageWithNode, buildImage, checkPnpm } from './common';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
const { const {
applicationId,
buildId, buildId,
tag,
workdir, workdir,
publishDirectory,
port, port,
installCommand, installCommand,
buildCommand, buildCommand,
startCommand, startCommand,
baseDirectory, baseDirectory,
secrets, secrets,
pullmergeRequestId pullmergeRequestId,
deploymentType,
baseImage
} = data; } = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
const isPnpm = checkPnpm(installCommand, buildCommand, startCommand); const isPnpm = checkPnpm(installCommand, buildCommand, startCommand);
@ -36,22 +41,35 @@ const createDockerfile = async (data, image): Promise<void> => {
if (isPnpm) { if (isPnpm) {
Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7'); Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7');
} }
Dockerfile.push(`COPY .${baseDirectory || ''} ./`); if (deploymentType === 'node') {
Dockerfile.push(`RUN ${installCommand}`); Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
Dockerfile.push(`RUN ${installCommand}`);
if (buildCommand) {
Dockerfile.push(`RUN ${buildCommand}`); 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`);
} }
Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push(`CMD ${startCommand}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };
export default async function (data) { export default async function (data) {
try { try {
const { baseImage, baseBuildImage } = data; const { baseImage, baseBuildImage, deploymentType, buildCommand } = data;
await createDockerfile(data, baseImage); if (deploymentType === 'node') {
await buildImage(data); 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) { } catch (error) {
throw error; throw error;
} }

View File

@ -31,6 +31,31 @@ export async function listApplications(request: FastifyRequest) {
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function getImages(request: FastifyRequest) {
try {
const { buildPack, deploymentType } = request.body
let publishDirectory = undefined;
let port = undefined
const { baseImage, baseBuildImage, baseBuildImages, baseImages, } = setDefaultBaseImage(
buildPack, deploymentType
);
if (buildPack === 'nextjs') {
if (deploymentType === 'static') {
publishDirectory = 'out'
port = '80'
} else {
publishDirectory = ''
port = '3000'
}
}
return { baseImage, baseBuildImage, baseBuildImages, baseImages, publishDirectory, port }
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getApplication(request: FastifyRequest<GetApplication>) { export async function getApplication(request: FastifyRequest<GetApplication>) {
try { try {
const { id } = request.params const { id } = request.params
@ -184,7 +209,8 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
denoMainFile, denoMainFile,
denoOptions, denoOptions,
baseImage, baseImage,
baseBuildImage baseBuildImage,
deploymentType
} = request.body } = request.body
if (port) port = Number(port); if (port) port = Number(port);
@ -215,6 +241,7 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
denoOptions, denoOptions,
baseImage, baseImage,
baseBuildImage, baseBuildImage,
deploymentType,
...defaultConfiguration ...defaultConfiguration
} }
}); });

View File

@ -1,5 +1,5 @@
import { FastifyPluginAsync } from 'fastify'; import { FastifyPluginAsync } from 'fastify';
import { cancelDeployment, checkDNS, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getBuildIdLogs, getBuildLogs, getBuildPack, getGitHubToken, getGitLabSSHKey, getPreviews, getSecrets, getStorages, getUsage, listApplications, newApplication, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication } from './handlers'; import { cancelDeployment, checkDNS, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getBuildIdLogs, getBuildLogs, getBuildPack, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getSecrets, getStorages, getUsage, listApplications, newApplication, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication } from './handlers';
export interface GetApplication { export interface GetApplication {
Params: { id: string; } Params: { id: string; }
@ -37,6 +37,7 @@ const root: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
return await request.jwtVerify() return await request.jwtVerify()
}) })
fastify.get('/', async (request) => await listApplications(request)); fastify.get('/', async (request) => await listApplications(request));
fastify.post('/images', async (request) => await getImages(request));
fastify.post('/new', async (request, reply) => await newApplication(request, reply)); fastify.post('/new', async (request, reply) => await newApplication(request, reply));
@ -67,7 +68,7 @@ const root: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
fastify.post<DeployApplication>('/:id/deploy', async (request) => await deployApplication(request)) fastify.post<DeployApplication>('/:id/deploy', async (request) => await deployApplication(request))
fastify.post('/:id/cancel', async (request, reply) => await cancelDeployment(request, reply)); fastify.post('/:id/cancel', async (request, reply) => await cancelDeployment(request, reply));
fastify.post('/:id/configuration/source', async (request, reply) => await saveApplicationSource(request, reply)); fastify.post('/:id/configuration/source', async (request, reply) => await saveApplicationSource(request, reply));
fastify.get('/:id/configuration/repository', async (request) => await checkRepository(request)); fastify.get('/:id/configuration/repository', async (request) => await checkRepository(request));

View File

@ -70,7 +70,8 @@ export function findBuildPack(pack: string, packageManager = 'npm') {
...metaData, ...metaData,
...defaultBuildAndDeploy(packageManager), ...defaultBuildAndDeploy(packageManager),
publishDirectory: null, publishDirectory: null,
port: 3000 port: 3000,
deploymentType: 'node'
}; };
} }
if (pack === 'gatsby') { if (pack === 'gatsby') {

View File

@ -60,7 +60,7 @@
} }
} }
} }
async function scanRepository() { async function scanRepository(): Promise<void> {
try { try {
if (type === 'gitlab') { if (type === 'gitlab') {
const files = await get(`${apiUrl}/v4/projects/${projectId}/repository/tree`, { const files = await get(`${apiUrl}/v4/projects/${projectId}/repository/tree`, {

View File

@ -103,8 +103,18 @@
usageInterval = setInterval(async () => { usageInterval = setInterval(async () => {
await getUsage(); await getUsage();
}, 1000); }, 1000);
await getBaseBuildImages();
}); });
async function getBaseBuildImages() {
const data = await post(`/applications/images`, {
buildPack: application.buildPack,
deploymentType: application.deploymentType
});
application = {
...application,
...data
};
}
async function changeSettings(name: any) { async function changeSettings(name: any) {
if (name === 'debug') { if (name === 'debug') {
debug = !debug; debug = !debug;
@ -145,10 +155,12 @@
} }
} }
async function handleSubmit() { async function handleSubmit() {
if (loading) return; if (loading || !application.fqdn) return;
loading = true; loading = true;
try { try {
nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, ''); nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
if (application.deploymentType)
application.deploymentType = application.deploymentType.toLowerCase();
await post(`/applications/${id}/check`, { await post(`/applications/${id}/check`, {
fqdn: application.fqdn, fqdn: application.fqdn,
forceSave, forceSave,
@ -192,6 +204,11 @@
application.baseBuildImage = event.detail.value; application.baseBuildImage = event.detail.value;
await handleSubmit(); await handleSubmit();
} }
async function selectDeploymentType(event: any) {
application.deploymentType = event.detail.value;
await getBaseBuildImages();
await handleSubmit();
}
async function isDNSValid(domain: any, isWWW: any) { async function isDNSValid(domain: any, isWWW: any) {
try { try {
@ -404,26 +421,6 @@
/> />
</div> </div>
</div> </div>
{#if application.buildPack !== 'docker'}
<div class="grid grid-cols-2 items-center">
<label for="baseImage" class="text-base font-bold text-stone-100"
>{$t('application.base_image')}</label
>
<div class="custom-select-wrapper">
<Select
{isDisabled}
containerClasses={isDisabled && containerClass()}
id="baseImages"
showIndicator={!$status.application.isRunning}
items={application.baseImages}
on:select={selectBaseImage}
value={application.baseImage}
isClearable={false}
/>
</div>
<Explainer text={$t('application.base_image_explainer')} />
</div>
{/if}
{#if application.buildCommand || application.buildPack === 'rust' || application.buildPack === 'laravel'} {#if application.buildCommand || application.buildPack === 'rust' || application.buildPack === 'laravel'}
<div class="grid grid-cols-2 items-center pb-8"> <div class="grid grid-cols-2 items-center pb-8">
<label for="baseBuildImage" class="text-base font-bold text-stone-100" <label for="baseBuildImage" class="text-base font-bold text-stone-100"
@ -449,6 +446,48 @@
{/if} {/if}
</div> </div>
{/if} {/if}
{#if application.buildPack !== 'docker'}
<div class="grid grid-cols-2 items-center">
<label for="baseImage" class="text-base font-bold text-stone-100"
>{$t('application.base_image')}</label
>
<div class="custom-select-wrapper">
<Select
{isDisabled}
containerClasses={isDisabled && containerClass()}
id="baseImages"
showIndicator={!$status.application.isRunning}
items={application.baseImages}
on:select={selectBaseImage}
value={application.baseImage}
isClearable={false}
/>
</div>
<Explainer text={$t('application.base_image_explainer')} />
</div>
{/if}
{#if application.buildPack !== 'docker' && application.buildPack === 'nextjs'}
<div class="grid grid-cols-2 items-center pb-8">
<label for="deploymentType" class="text-base font-bold text-stone-100"
>Deployment Type</label
>
<div class="custom-select-wrapper">
<Select
{isDisabled}
containerClasses={isDisabled && containerClass()}
id="deploymentTypes"
showIndicator={!$status.application.isRunning}
items={['static', 'node']}
on:select={selectDeploymentType}
value={application.deploymentType}
isClearable={false}
/>
</div>
<Explainer
text="How to build your application. Static is for static websites, node is for server-side applications."
/>
</div>
{/if}
</div> </div>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">{$t('application.application')}</div> <div class="title">{$t('application.application')}</div>
@ -473,6 +512,7 @@
bind:this={domainEl} bind:this={domainEl}
name="fqdn" name="fqdn"
id="fqdn" id="fqdn"
required
bind:value={application.fqdn} bind:value={application.fqdn}
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$" pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="eg: https://coollabs.io" placeholder="eg: https://coollabs.io"
@ -516,7 +556,7 @@
<div class="grid grid-cols-2 items-center pb-8"> <div class="grid grid-cols-2 items-center pb-8">
<Setting <Setting
dataTooltip={$t('forms.must_be_stopped_to_modify')} dataTooltip={$t('forms.must_be_stopped_to_modify')}
disabled={$status.application.isRunning} disabled={isDisabled}
isCenter={false} isCenter={false}
bind:setting={dualCerts} bind:setting={dualCerts}
title={$t('application.ssl_www_and_non_www')} title={$t('application.ssl_www_and_non_www')}
@ -535,6 +575,7 @@
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="pythonModule" class="text-base font-bold text-stone-100">Module</label> <label for="pythonModule" class="text-base font-bold text-stone-100">Module</label>
<input <input
disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="pythonModule" name="pythonModule"
id="pythonModule" id="pythonModule"
@ -547,6 +588,7 @@
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="pythonVariable" class="text-base font-bold text-stone-100">Variable</label> <label for="pythonVariable" class="text-base font-bold text-stone-100">Variable</label>
<input <input
disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="pythonVariable" name="pythonVariable"
id="pythonVariable" id="pythonVariable"
@ -560,6 +602,7 @@
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="pythonVariable" class="text-base font-bold text-stone-100">Variable</label> <label for="pythonVariable" class="text-base font-bold text-stone-100">Variable</label>
<input <input
disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="pythonVariable" name="pythonVariable"
id="pythonVariable" id="pythonVariable"
@ -574,6 +617,7 @@
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="port" class="text-base font-bold text-stone-100">{$t('forms.port')}</label> <label for="port" class="text-base font-bold text-stone-100">{$t('forms.port')}</label>
<input <input
disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="port" name="port"
id="port" id="port"
@ -604,6 +648,7 @@
>{$t('application.install_command')}</label >{$t('application.install_command')}</label
> >
<input <input
disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="installCommand" name="installCommand"
id="installCommand" id="installCommand"
@ -616,6 +661,7 @@
>{$t('application.build_command')}</label >{$t('application.build_command')}</label
> >
<input <input
disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="buildCommand" name="buildCommand"
id="buildCommand" id="buildCommand"
@ -628,6 +674,7 @@
>{$t('application.start_command')}</label >{$t('application.start_command')}</label
> >
<input <input
disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="startCommand" name="startCommand"
id="startCommand" id="startCommand"
@ -642,6 +689,7 @@
>Dockerfile Location</label >Dockerfile Location</label
> >
<input <input
disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="dockerFileLocation" name="dockerFileLocation"
id="dockerFileLocation" id="dockerFileLocation"
@ -657,6 +705,7 @@
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="denoMainFile" class="text-base font-bold text-stone-100">Main File</label> <label for="denoMainFile" class="text-base font-bold text-stone-100">Main File</label>
<input <input
disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="denoMainFile" name="denoMainFile"
id="denoMainFile" id="denoMainFile"
@ -667,6 +716,7 @@
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="denoOptions" class="text-base font-bold text-stone-100">Arguments</label> <label for="denoOptions" class="text-base font-bold text-stone-100">Arguments</label>
<input <input
disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="denoOptions" name="denoOptions"
id="denoOptions" id="denoOptions"
@ -687,6 +737,7 @@
<Explainer text={$t('application.directory_to_use_explainer')} /> <Explainer text={$t('application.directory_to_use_explainer')} />
</div> </div>
<input <input
disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="baseDirectory" name="baseDirectory"
id="baseDirectory" id="baseDirectory"
@ -705,9 +756,11 @@
</div> </div>
<input <input
disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="publishDirectory" name="publishDirectory"
id="publishDirectory" id="publishDirectory"
required={application.deploymentType === 'static'}
bind:value={application.publishDirectory} bind:value={application.publishDirectory}
placeholder=" {$t('forms.default')}: /" placeholder=" {$t('forms.default')}: /"
/> />

View File

@ -7,6 +7,7 @@
"db:studio": "pnpm run --filter coolify-api db:studio", "db:studio": "pnpm run --filter coolify-api db:studio",
"db:push": "pnpm run --filter coolify-api db:push", "db:push": "pnpm run --filter coolify-api db:push",
"db:seed": "pnpm run --filter coolify-api db:seed", "db:seed": "pnpm run --filter coolify-api db:seed",
"db:migrate": "pnpm run --filter coolify-api db:migrate",
"format": "run-p -l -n format:*", "format": "run-p -l -n format:*",
"format:api": "NODE_ENV=development pnpm run --filter coolify-api format", "format:api": "NODE_ENV=development pnpm run --filter coolify-api format",
"lint": "run-p -l -n lint:*", "lint": "run-p -l -n lint:*",