feat: initial python support
This commit is contained in:
4
prisma/migrations/20220402135305_python/migration.sql
Normal file
4
prisma/migrations/20220402135305_python/migration.sql
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Application" ADD COLUMN "pythonModule" TEXT;
|
||||||
|
ALTER TABLE "Application" ADD COLUMN "pythonVariable" TEXT;
|
||||||
|
ALTER TABLE "Application" ADD COLUMN "pythonWSGI" TEXT;
|
@@ -87,6 +87,9 @@ model Application {
|
|||||||
baseDirectory String?
|
baseDirectory String?
|
||||||
publishDirectory String?
|
publishDirectory String?
|
||||||
phpModules String?
|
phpModules String?
|
||||||
|
pythonWSGI String?
|
||||||
|
pythonModule String?
|
||||||
|
pythonVariable String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
settings ApplicationSettings?
|
settings ApplicationSettings?
|
||||||
|
@@ -100,10 +100,14 @@ export const setDefaultConfiguration = async (data) => {
|
|||||||
if (buildPack === 'static') port = 80;
|
if (buildPack === 'static') port = 80;
|
||||||
else if (buildPack === 'node') port = 3000;
|
else if (buildPack === 'node') port = 3000;
|
||||||
else if (buildPack === 'php') port = 80;
|
else if (buildPack === 'php') port = 80;
|
||||||
|
else if (buildPack === 'python') port = 8000;
|
||||||
}
|
}
|
||||||
if (!installCommand) installCommand = template?.installCommand || 'yarn install';
|
if (template) {
|
||||||
if (!startCommand) startCommand = template?.startCommand || 'yarn start';
|
if (!installCommand) installCommand = template?.installCommand || 'yarn install';
|
||||||
if (!buildCommand) buildCommand = template?.buildCommand || null;
|
if (!startCommand) startCommand = template?.startCommand || 'yarn start';
|
||||||
|
if (!buildCommand) buildCommand = template?.buildCommand || null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!publishDirectory) publishDirectory = template?.publishDirectory || null;
|
if (!publishDirectory) publishDirectory = template?.publishDirectory || null;
|
||||||
if (baseDirectory) {
|
if (baseDirectory) {
|
||||||
if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`;
|
if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`;
|
||||||
|
@@ -12,6 +12,7 @@ import php from './php';
|
|||||||
import rust from './rust';
|
import rust from './rust';
|
||||||
import astro from './static';
|
import astro from './static';
|
||||||
import eleventy from './static';
|
import eleventy from './static';
|
||||||
|
import python from './python';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
node,
|
node,
|
||||||
@@ -27,5 +28,6 @@ export {
|
|||||||
php,
|
php,
|
||||||
rust,
|
rust,
|
||||||
astro,
|
astro,
|
||||||
eleventy
|
eleventy,
|
||||||
|
python
|
||||||
};
|
};
|
||||||
|
73
src/lib/buildPacks/python.ts
Normal file
73
src/lib/buildPacks/python.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { buildImage } from '$lib/docker';
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
|
|
||||||
|
const createDockerfile = async (data, image): Promise<void> => {
|
||||||
|
const {
|
||||||
|
workdir,
|
||||||
|
port,
|
||||||
|
baseDirectory,
|
||||||
|
secrets,
|
||||||
|
pullmergeRequestId,
|
||||||
|
pythonWSGI,
|
||||||
|
pythonModule,
|
||||||
|
pythonVariable
|
||||||
|
} = data;
|
||||||
|
const Dockerfile: Array<string> = [];
|
||||||
|
Dockerfile.push(`FROM ${image}`);
|
||||||
|
Dockerfile.push('WORKDIR /app');
|
||||||
|
Dockerfile.push(`LABEL coolify.image=true`);
|
||||||
|
if (secrets.length > 0) {
|
||||||
|
secrets.forEach((secret) => {
|
||||||
|
if (secret.isBuildSecret) {
|
||||||
|
if (pullmergeRequestId) {
|
||||||
|
if (secret.isPRMRSecret) {
|
||||||
|
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!secret.isPRMRSecret) {
|
||||||
|
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (pythonWSGI?.toLowerCase() === 'gunicorn') {
|
||||||
|
Dockerfile.push(`RUN pip install gunicorn`);
|
||||||
|
} 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}`);
|
||||||
|
console.log({ pythonWSGI });
|
||||||
|
if (pythonWSGI?.toLowerCase() === 'gunicorn') {
|
||||||
|
console.log({ pythonModule, pythonVariable });
|
||||||
|
Dockerfile.push(`CMD gunicorn -w=4 -b=0.0.0.0:8000 ${pythonModule}:${pythonVariable}`);
|
||||||
|
} 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 image = 'python:3-alpine';
|
||||||
|
await createDockerfile(data, image);
|
||||||
|
await buildImage(data);
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
@@ -19,7 +19,7 @@ export const staticDeployments = [
|
|||||||
'astro',
|
'astro',
|
||||||
'eleventy'
|
'eleventy'
|
||||||
];
|
];
|
||||||
export const notNodeDeployments = ['php', 'docker', 'rust'];
|
export const notNodeDeployments = ['php', 'docker', 'rust', 'python'];
|
||||||
|
|
||||||
export function getDomain(domain) {
|
export function getDomain(domain) {
|
||||||
return domain?.replace('https://', '').replace('http://', '');
|
return domain?.replace('https://', '').replace('http://', '');
|
||||||
|
@@ -146,6 +146,13 @@ export function findBuildPack(pack, packageManager = 'npm') {
|
|||||||
port: 80
|
port: 80
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (pack === 'python') {
|
||||||
|
return {
|
||||||
|
...metaData,
|
||||||
|
startCommand: null,
|
||||||
|
port: 8000
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
name: 'node',
|
name: 'node',
|
||||||
fancyName: 'Node.js',
|
fancyName: 'Node.js',
|
||||||
@@ -249,6 +256,12 @@ export const buildPacks = [
|
|||||||
fancyName: 'Rust',
|
fancyName: 'Rust',
|
||||||
hoverColor: 'hover:bg-pink-700',
|
hoverColor: 'hover:bg-pink-700',
|
||||||
color: 'bg-pink-700'
|
color: 'bg-pink-700'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'python',
|
||||||
|
fancyName: 'Python',
|
||||||
|
hoverColor: 'hover:bg-green-700',
|
||||||
|
color: 'bg-green-700'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
export const scanningTemplates = {
|
export const scanningTemplates = {
|
||||||
|
@@ -214,11 +214,15 @@ export async function configureApplication({
|
|||||||
buildCommand,
|
buildCommand,
|
||||||
startCommand,
|
startCommand,
|
||||||
baseDirectory,
|
baseDirectory,
|
||||||
publishDirectory
|
publishDirectory,
|
||||||
|
pythonWSGI,
|
||||||
|
pythonModule,
|
||||||
|
pythonVariable
|
||||||
}) {
|
}) {
|
||||||
return await prisma.application.update({
|
return await prisma.application.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
|
name,
|
||||||
buildPack,
|
buildPack,
|
||||||
fqdn,
|
fqdn,
|
||||||
port,
|
port,
|
||||||
@@ -227,7 +231,9 @@ export async function configureApplication({
|
|||||||
startCommand,
|
startCommand,
|
||||||
baseDirectory,
|
baseDirectory,
|
||||||
publishDirectory,
|
publishDirectory,
|
||||||
name
|
pythonWSGI,
|
||||||
|
pythonModule,
|
||||||
|
pythonVariable
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -51,7 +51,10 @@ export default async function (job) {
|
|||||||
pullmergeRequestId = null,
|
pullmergeRequestId = null,
|
||||||
sourceBranch = null,
|
sourceBranch = null,
|
||||||
settings,
|
settings,
|
||||||
persistentStorage
|
persistentStorage,
|
||||||
|
pythonWSGI,
|
||||||
|
pythonModule,
|
||||||
|
pythonVariable
|
||||||
} = job.data;
|
} = job.data;
|
||||||
const { debug } = settings;
|
const { debug } = settings;
|
||||||
|
|
||||||
@@ -127,7 +130,7 @@ export default async function (job) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.prisma.build.update({ where: { id: buildId }, data: { commit } });
|
await db.prisma.build.update({ where: { id: buildId }, data: { commit } });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
@@ -200,7 +203,10 @@ export default async function (job) {
|
|||||||
startCommand,
|
startCommand,
|
||||||
baseDirectory,
|
baseDirectory,
|
||||||
secrets,
|
secrets,
|
||||||
phpModules
|
phpModules,
|
||||||
|
pythonWSGI,
|
||||||
|
pythonModule,
|
||||||
|
pythonVariable
|
||||||
});
|
});
|
||||||
else {
|
else {
|
||||||
saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId });
|
saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId });
|
||||||
|
@@ -5,6 +5,7 @@ import { checkContainer } from '$lib/haproxy';
|
|||||||
import type { RequestHandler } from '@sveltejs/kit';
|
import type { RequestHandler } from '@sveltejs/kit';
|
||||||
import jsonwebtoken from 'jsonwebtoken';
|
import jsonwebtoken from 'jsonwebtoken';
|
||||||
import { get as getRequest } from '$lib/api';
|
import { get as getRequest } from '$lib/api';
|
||||||
|
import { setDefaultConfiguration } from '$lib/buildPacks/common';
|
||||||
|
|
||||||
export const get: RequestHandler = async (event) => {
|
export const get: RequestHandler = async (event) => {
|
||||||
const { teamId, status, body } = await getUserDetails(event);
|
const { teamId, status, body } = await getUserDetails(event);
|
||||||
@@ -52,12 +53,23 @@ export const post: RequestHandler = async (event) => {
|
|||||||
buildCommand,
|
buildCommand,
|
||||||
startCommand,
|
startCommand,
|
||||||
baseDirectory,
|
baseDirectory,
|
||||||
publishDirectory
|
publishDirectory,
|
||||||
|
pythonWSGI,
|
||||||
|
pythonModule,
|
||||||
|
pythonVariable
|
||||||
} = await event.request.json();
|
} = await event.request.json();
|
||||||
|
|
||||||
if (port) port = Number(port);
|
if (port) port = Number(port);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const defaultConfiguration = await setDefaultConfiguration({
|
||||||
|
buildPack,
|
||||||
|
port,
|
||||||
|
installCommand,
|
||||||
|
startCommand,
|
||||||
|
buildCommand,
|
||||||
|
publishDirectory,
|
||||||
|
baseDirectory
|
||||||
|
});
|
||||||
await db.configureApplication({
|
await db.configureApplication({
|
||||||
id,
|
id,
|
||||||
buildPack,
|
buildPack,
|
||||||
@@ -68,7 +80,11 @@ export const post: RequestHandler = async (event) => {
|
|||||||
buildCommand,
|
buildCommand,
|
||||||
startCommand,
|
startCommand,
|
||||||
baseDirectory,
|
baseDirectory,
|
||||||
publishDirectory
|
publishDirectory,
|
||||||
|
pythonWSGI,
|
||||||
|
pythonModule,
|
||||||
|
pythonVariable,
|
||||||
|
...defaultConfiguration
|
||||||
});
|
});
|
||||||
return { status: 201 };
|
return { status: 201 };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
import { page, session } from '$app/stores';
|
import { page, session } from '$app/stores';
|
||||||
import { errorNotification } from '$lib/form';
|
import { errorNotification } from '$lib/form';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import Select from 'svelte-select';
|
||||||
|
|
||||||
import Explainer from '$lib/components/Explainer.svelte';
|
import Explainer from '$lib/components/Explainer.svelte';
|
||||||
import Setting from '$lib/components/Setting.svelte';
|
import Setting from '$lib/components/Setting.svelte';
|
||||||
@@ -57,6 +58,23 @@
|
|||||||
let previews = application.settings.previews;
|
let previews = application.settings.previews;
|
||||||
let dualCerts = application.settings.dualCerts;
|
let dualCerts = application.settings.dualCerts;
|
||||||
let autodeploy = application.settings.autodeploy;
|
let autodeploy = application.settings.autodeploy;
|
||||||
|
|
||||||
|
let wsgis = [
|
||||||
|
{
|
||||||
|
value: 'None',
|
||||||
|
label: 'None'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Gunicorn',
|
||||||
|
label: 'Gunicorn'
|
||||||
|
}
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// value: 'uWSGI',
|
||||||
|
// label: 'uWSGI'
|
||||||
|
// }
|
||||||
|
];
|
||||||
|
|
||||||
if (browser && window.location.hostname === 'demo.coolify.io' && !application.fqdn) {
|
if (browser && window.location.hostname === 'demo.coolify.io' && !application.fqdn) {
|
||||||
application.fqdn = `http://${cuid()}.demo.coolify.io`;
|
application.fqdn = `http://${cuid()}.demo.coolify.io`;
|
||||||
}
|
}
|
||||||
@@ -119,6 +137,9 @@
|
|||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async function selectWSGI(event) {
|
||||||
|
application.pythonWSGI = event.detail.value;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
|
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
|
||||||
@@ -315,6 +336,39 @@
|
|||||||
on:click={() => !isRunning && changeSettings('dualCerts')}
|
on:click={() => !isRunning && changeSettings('dualCerts')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{#if application.buildPack === 'python'}
|
||||||
|
<div class="grid grid-cols-2 items-center">
|
||||||
|
<label for="pythonModule" class="text-base font-bold text-stone-100">WSGI</label>
|
||||||
|
<div class="custom-select-wrapper">
|
||||||
|
<Select id="wsgi" items={wsgis} on:select={selectWSGI} value={application.pythonWSGI} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 items-center">
|
||||||
|
<label for="pythonModule" class="text-base font-bold text-stone-100">Module</label>
|
||||||
|
<input
|
||||||
|
readonly={!$session.isAdmin}
|
||||||
|
name="pythonModule"
|
||||||
|
id="pythonModule"
|
||||||
|
required
|
||||||
|
bind:value={application.pythonModule}
|
||||||
|
placeholder={application.pythonWSGI?.toLowerCase() !== 'gunicorn'
|
||||||
|
? 'myapp.py'
|
||||||
|
: 'myapp'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 items-center">
|
||||||
|
<label for="pythonVariable" class="text-base font-bold text-stone-100">Variable</label>
|
||||||
|
<input
|
||||||
|
readonly={!$session.isAdmin}
|
||||||
|
name="pythonVariable"
|
||||||
|
id="pythonVariable"
|
||||||
|
required
|
||||||
|
bind:value={application.pythonVariable}
|
||||||
|
placeholder="default: app"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{#if !staticDeployments.includes(application.buildPack)}
|
{#if !staticDeployments.includes(application.buildPack)}
|
||||||
<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">Port</label>
|
<label for="port" class="text-base font-bold text-stone-100">Port</label>
|
||||||
@@ -323,7 +377,7 @@
|
|||||||
name="port"
|
name="port"
|
||||||
id="port"
|
id="port"
|
||||||
bind:value={application.port}
|
bind:value={application.port}
|
||||||
placeholder="default: 3000"
|
placeholder={application.buildPack === 'python' ? '8000' : '3000'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@@ -54,10 +54,10 @@ textarea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#svelte .item.hover {
|
#svelte .item.hover {
|
||||||
@apply bg-coolgray-100 text-white;
|
@apply bg-coollabs text-white !important;
|
||||||
}
|
}
|
||||||
#svelte .item.active {
|
#svelte .item.active {
|
||||||
@apply bg-coollabs text-white;
|
@apply bg-coolgray-100 text-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
|
Reference in New Issue
Block a user