diff --git a/.husky/pre-commit b/.husky/pre-commit index dc0378c34..fab6428a1 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -yarn lint-staged \ No newline at end of file +pnpm lint-staged diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 95266cb84..f0bde201e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,7 +35,8 @@ This is a little list of what you can do to help the project: - Copy `.env.template` to `.env` and set the `COOLIFY_APP_ID` environment variable to something cool. - Install dependencies with `pnpm install`. - Need to create a local SQlite database with `pnpm db:push`. - - This will apply all migrations and seed the database at `db/dev.db`. + - This will apply all migrations at `db/dev.db`. +- Seed the database with base entities with `pnpm db:seed` - You can start coding after starting `pnpm dev`. #### How to start after you set up your local fork? diff --git a/README.md b/README.md index 7d02bca45..2cb2d5bf6 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ An open-source & self-hostable Heroku / Netlify alternative. https://demo.coolify.io/ -(If it is unresponsible, that means someone overloaded the server. 🙃) +(If it is unresponsive, that means someone overloaded the server. 🙃) ## How to install diff --git a/package.json b/package.json index 2ac79d547..b1c47ec86 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "coolify", "description": "An open-source & self-hostable Heroku / Netlify alternative.", - "version": "2.2.7", + "version": "2.3.0", "license": "AGPL-3.0", "scripts": { "dev": "docker-compose -f docker-compose-dev.yaml up -d && cross-env NODE_ENV=development & svelte-kit dev", @@ -30,14 +30,14 @@ "@sveltejs/kit": "1.0.0-next.303", "@types/bcrypt": "5.0.0", "@types/js-cookie": "3.0.1", - "@types/js-yaml": "^4.0.5", + "@types/js-yaml": "4.0.5", "@types/node": "17.0.23", "@types/node-forge": "1.0.1", "@typescript-eslint/eslint-plugin": "4.31.1", "@typescript-eslint/parser": "4.31.1", "@zerodevx/svelte-toast": "0.7.1", "autoprefixer": "10.4.4", - "cross-env": "^7.0.3", + "cross-env": "7.0.3", "cross-var": "1.1.0", "eslint": "7.32.0", "eslint-config-prettier": "8.5.0", @@ -52,8 +52,8 @@ "svelte": "3.46.4", "svelte-check": "2.4.6", "svelte-preprocess": "4.10.4", - "svelte-select": "^4.4.7", - "sveltekit-i18n": "^2.1.2", + "svelte-select": "4.4.7", + "sveltekit-i18n": "2.1.2", "tailwindcss": "3.0.23", "ts-node": "10.7.0", "tslib": "2.3.1", @@ -68,7 +68,7 @@ "bullmq": "1.78.1", "compare-versions": "4.1.3", "cookie": "0.4.2", - "cooltipz-css": "^2.1.0", + "cooltipz-css": "2.1.0", "cuid": "2.1.8", "dayjs": "1.11.0", "dockerode": "3.3.1", @@ -79,10 +79,11 @@ "js-cookie": "3.0.1", "js-yaml": "4.1.0", "jsonwebtoken": "8.5.1", - "mustache": "^4.2.0", + "mustache": "4.2.0", "node-forge": "1.3.0", + "p-limit": "4.0.0", "svelte-kit-cookie-session": "2.1.2", - "tailwindcss-scrollbar": "^0.1.0", + "tailwindcss-scrollbar": "0.1.0", "unique-names-generator": "4.7.1" }, "prisma": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0a432406d..581de6bd7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,7 +8,7 @@ specifiers: '@sveltejs/kit': 1.0.0-next.303 '@types/bcrypt': 5.0.0 '@types/js-cookie': 3.0.1 - '@types/js-yaml': ^4.0.5 + '@types/js-yaml': 4.0.5 '@types/node': 17.0.23 '@types/node-forge': 1.0.1 '@typescript-eslint/eslint-plugin': 4.31.1 @@ -19,8 +19,8 @@ specifiers: bullmq: 1.78.1 compare-versions: 4.1.3 cookie: 0.4.2 - cooltipz-css: ^2.1.0 - cross-env: ^7.0.3 + cooltipz-css: 2.1.0 + cross-env: 7.0.3 cross-var: 1.1.0 cuid: 2.1.8 dayjs: 1.11.0 @@ -37,8 +37,9 @@ specifiers: js-yaml: 4.1.0 jsonwebtoken: 8.5.1 lint-staged: 12.3.7 - mustache: ^4.2.0 + mustache: 4.2.0 node-forge: 1.3.0 + p-limit: 4.0.0 postcss: 8.4.12 prettier: 2.6.1 prettier-plugin-svelte: 2.6.0 @@ -48,10 +49,10 @@ specifiers: svelte-check: 2.4.6 svelte-kit-cookie-session: 2.1.2 svelte-preprocess: 4.10.4 - svelte-select: ^4.4.7 - sveltekit-i18n: ^2.1.2 + svelte-select: 4.4.7 + sveltekit-i18n: 2.1.2 tailwindcss: 3.0.23 - tailwindcss-scrollbar: ^0.1.0 + tailwindcss-scrollbar: 0.1.0 ts-node: 10.7.0 tslib: 2.3.1 typescript: 4.6.3 @@ -78,6 +79,7 @@ dependencies: jsonwebtoken: 8.5.1 mustache: 4.2.0 node-forge: 1.3.0 + p-limit: 4.0.0 svelte-kit-cookie-session: 2.1.2 tailwindcss-scrollbar: 0.1.0_tailwindcss@3.0.23 unique-names-generator: 4.7.1 @@ -4270,6 +4272,16 @@ packages: engines: { node: '>=12.20' } dev: false + /p-limit/4.0.0: + resolution: + { + integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ== + } + engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } + dependencies: + yocto-queue: 1.0.0 + dev: false + /p-map/2.1.0: resolution: { @@ -5714,3 +5726,11 @@ packages: } engines: { node: '>=6' } dev: true + + /yocto-queue/1.0.0: + resolution: + { + integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== + } + engines: { node: '>=12.20' } + dev: false diff --git a/prisma/migrations/20220402135305_python/migration.sql b/prisma/migrations/20220402135305_python/migration.sql new file mode 100644 index 000000000..0c253d539 --- /dev/null +++ b/prisma/migrations/20220402135305_python/migration.sql @@ -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; diff --git a/prisma/migrations/20220402210645_meilisearch/migration.sql b/prisma/migrations/20220402210645_meilisearch/migration.sql new file mode 100644 index 000000000..9e832b107 --- /dev/null +++ b/prisma/migrations/20220402210645_meilisearch/migration.sql @@ -0,0 +1,12 @@ +-- CreateTable +CREATE TABLE "MeiliSearch" ( + "id" TEXT NOT NULL PRIMARY KEY, + "masterKey" TEXT NOT NULL, + "serviceId" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "MeiliSearch_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "MeiliSearch_serviceId_key" ON "MeiliSearch"("serviceId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a849ae36f..986a773bf 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -87,6 +87,9 @@ model Application { baseDirectory String? publishDirectory String? phpModules String? + pythonWSGI String? + pythonModule String? + pythonVariable String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt settings ApplicationSettings? @@ -280,6 +283,7 @@ model Service { wordpress Wordpress? ghost Ghost? serviceSecret ServiceSecret[] + meiliSearch MeiliSearch? } model PlausibleAnalytics { @@ -349,3 +353,12 @@ model Ghost { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } + +model MeiliSearch { + id String @id @default(cuid()) + masterKey String + serviceId String @unique + service Service @relation(fields: [serviceId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/src/lib/buildPacks/common.ts b/src/lib/buildPacks/common.ts index 3f8b4b43e..0fa4f936a 100644 --- a/src/lib/buildPacks/common.ts +++ b/src/lib/buildPacks/common.ts @@ -100,10 +100,14 @@ export const setDefaultConfiguration = async (data) => { if (buildPack === 'static') port = 80; else if (buildPack === 'node') port = 3000; else if (buildPack === 'php') port = 80; + else if (buildPack === 'python') port = 8000; } - if (!installCommand) installCommand = template?.installCommand || 'yarn install'; - if (!startCommand) startCommand = template?.startCommand || 'yarn start'; - if (!buildCommand) buildCommand = template?.buildCommand || null; + if (template) { + if (!installCommand) installCommand = template?.installCommand || 'yarn install'; + if (!startCommand) startCommand = template?.startCommand || 'yarn start'; + if (!buildCommand) buildCommand = template?.buildCommand || null; + } + if (!publishDirectory) publishDirectory = template?.publishDirectory || null; if (baseDirectory) { if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`; @@ -136,7 +140,11 @@ export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, ap ` ); await fs.writeFile(`${workdir}/entrypoint.sh`, `chown -R 1000 /app`); - saveBuildLog({ line: 'Copied default configuration file for PHP.', buildId, applicationId }); + await saveBuildLog({ + line: 'Copied default configuration file for PHP.', + buildId, + applicationId + }); } else if (staticDeployments.includes(buildPack)) { await fs.writeFile( `${workdir}/nginx.conf`, @@ -190,7 +198,7 @@ export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, ap } ` ); - saveBuildLog({ line: 'Copied default configuration file.', buildId, applicationId }); + await saveBuildLog({ line: 'Copied default configuration file.', buildId, applicationId }); } } catch (error) { console.log(error); diff --git a/src/lib/buildPacks/index.ts b/src/lib/buildPacks/index.ts index 41a7655f9..babbe8f17 100644 --- a/src/lib/buildPacks/index.ts +++ b/src/lib/buildPacks/index.ts @@ -12,6 +12,7 @@ import php from './php'; import rust from './rust'; import astro from './static'; import eleventy from './static'; +import python from './python'; export { node, @@ -27,5 +28,6 @@ export { php, rust, astro, - eleventy + eleventy, + python }; diff --git a/src/lib/buildPacks/python.ts b/src/lib/buildPacks/python.ts new file mode 100644 index 000000000..1c6bdf6bf --- /dev/null +++ b/src/lib/buildPacks/python.ts @@ -0,0 +1,71 @@ +import { buildImage } from '$lib/docker'; +import { promises as fs } from 'fs'; + +const createDockerfile = async (data, image): Promise => { + const { + workdir, + port, + baseDirectory, + secrets, + pullmergeRequestId, + pythonWSGI, + pythonModule, + pythonVariable + } = data; + const Dockerfile: Array = []; + 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}`); + if (pythonWSGI?.toLowerCase() === 'gunicorn') { + 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; + } +} diff --git a/src/lib/common.ts b/src/lib/common.ts index 68f85572f..49a637065 100644 --- a/src/lib/common.ts +++ b/src/lib/common.ts @@ -80,7 +80,7 @@ export const getTeam = (event) => { export const getUserDetails = async (event, isAdminRequired = true) => { const teamId = getTeam(event); - const userId = event.locals.session.data.userId || null; + const userId = event?.locals?.session?.data?.userId || null; const { permission = 'read' } = await db.prisma.permission.findFirst({ where: { teamId, userId }, select: { permission: true }, diff --git a/src/lib/components/DatabaseLinks.svelte b/src/lib/components/DatabaseLinks.svelte new file mode 100644 index 000000000..9ef11a238 --- /dev/null +++ b/src/lib/components/DatabaseLinks.svelte @@ -0,0 +1,25 @@ + + + + {#if database.type === 'clickhouse'} + + {:else if database.type === 'couchdb'} + + {:else if database.type === 'mongodb'} + + {:else if database.type === 'mysql'} + + {:else if database.type === 'postgresql'} + + {:else if database.type === 'redis'} + + {/if} + diff --git a/src/lib/components/ServiceLinks.svelte b/src/lib/components/ServiceLinks.svelte new file mode 100644 index 000000000..a3b4ce2cd --- /dev/null +++ b/src/lib/components/ServiceLinks.svelte @@ -0,0 +1,55 @@ + + +{#if service.type === 'plausibleanalytics'} + + + +{:else if service.type === 'nocodb'} + + + +{:else if service.type === 'minio'} + + + +{:else if service.type === 'vscodeserver'} + + + +{:else if service.type === 'wordpress'} + + + +{:else if service.type === 'vaultwarden'} + + + +{:else if service.type === 'languagetool'} + + + +{:else if service.type === 'n8n'} + + + +{:else if service.type === 'uptimekuma'} + + + +{:else if service.type === 'ghost'} + + + +{/if} diff --git a/src/lib/components/common.ts b/src/lib/components/common.ts index b9b3ac982..f6d0f232c 100644 --- a/src/lib/components/common.ts +++ b/src/lib/components/common.ts @@ -19,7 +19,7 @@ export const staticDeployments = [ 'astro', 'eleventy' ]; -export const notNodeDeployments = ['php', 'docker', 'rust']; +export const notNodeDeployments = ['php', 'docker', 'rust', 'python']; export function getDomain(domain) { return domain?.replace('https://', '').replace('http://', ''); @@ -37,3 +37,9 @@ export function dashify(str: string, options?: any): string { .replace(/-{2,}/g, (m) => (options && options.condense ? '-' : m)) .toLowerCase(); } + +export function changeQueryParams(buildId) { + const queryParams = new URLSearchParams(window.location.search); + queryParams.set('buildId', buildId); + return history.pushState(null, null, '?' + queryParams.toString()); +} diff --git a/src/lib/components/svg/services/MeiliSearch.svelte b/src/lib/components/svg/services/MeiliSearch.svelte new file mode 100644 index 000000000..b8d4ed788 --- /dev/null +++ b/src/lib/components/svg/services/MeiliSearch.svelte @@ -0,0 +1,45 @@ + + + diff --git a/src/lib/components/templates.ts b/src/lib/components/templates.ts index 5ad62f6fa..b82b12f51 100644 --- a/src/lib/components/templates.ts +++ b/src/lib/components/templates.ts @@ -146,6 +146,13 @@ export function findBuildPack(pack, packageManager = 'npm') { port: 80 }; } + if (pack === 'python') { + return { + ...metaData, + startCommand: null, + port: 8000 + }; + } return { name: 'node', fancyName: 'Node.js', @@ -249,6 +256,12 @@ export const buildPacks = [ fancyName: 'Rust', hoverColor: 'hover:bg-pink-700', color: 'bg-pink-700' + }, + { + name: 'python', + fancyName: 'Python', + hoverColor: 'hover:bg-green-700', + color: 'bg-green-700' } ]; export const scanningTemplates = { diff --git a/src/lib/database/applications.ts b/src/lib/database/applications.ts index ad03ed7c0..1d8140144 100644 --- a/src/lib/database/applications.ts +++ b/src/lib/database/applications.ts @@ -214,11 +214,15 @@ export async function configureApplication({ buildCommand, startCommand, baseDirectory, - publishDirectory + publishDirectory, + pythonWSGI, + pythonModule, + pythonVariable }) { return await prisma.application.update({ where: { id }, data: { + name, buildPack, fqdn, port, @@ -227,7 +231,9 @@ export async function configureApplication({ startCommand, baseDirectory, publishDirectory, - name + pythonWSGI, + pythonModule, + pythonVariable } }); } diff --git a/src/lib/database/common.ts b/src/lib/database/common.ts index e89105f05..3e473f094 100644 --- a/src/lib/database/common.ts +++ b/src/lib/database/common.ts @@ -46,7 +46,9 @@ export function ErrorHandler(e) { if (e.message?.includes('git clone')) { truncatedError.message = 'git clone failed'; } - sentry.captureException(truncatedError); + if (!e.message?.includes('Coolify Proxy is not running')) { + sentry.captureException(truncatedError); + } const payload = { status: truncatedError.status || 500, body: { @@ -195,6 +197,16 @@ export const supportedServiceTypesAndVersions = [ ports: { main: 2368 } + }, + { + name: 'meilisearch', + fancyName: 'Meilisearch', + baseImage: 'getmeili/meilisearch', + images: [], + versions: ['latest'], + ports: { + main: 7700 + } } ]; diff --git a/src/lib/database/services.ts b/src/lib/database/services.ts index a5c2ee9ea..c5d221e9e 100644 --- a/src/lib/database/services.ts +++ b/src/lib/database/services.ts @@ -22,7 +22,8 @@ export async function getService({ id, teamId }) { vscodeserver: true, wordpress: true, ghost: true, - serviceSecret: true + serviceSecret: true, + meiliSearch: true } }); @@ -50,6 +51,8 @@ export async function getService({ id, teamId }) { body.ghost.mariadbRootUserPassword = decrypt(body.ghost.mariadbRootUserPassword); if (body.ghost?.defaultPassword) body.ghost.defaultPassword = decrypt(body.ghost.defaultPassword); + if (body.meiliSearch?.masterKey) body.meiliSearch.masterKey = decrypt(body.meiliSearch.masterKey); + if (body?.serviceSecret.length > 0) { body.serviceSecret = body.serviceSecret.map((s) => { s.value = decrypt(s.value); @@ -165,6 +168,15 @@ export async function configureServiceType({ id, type }) { } } }); + } else if (type === 'meilisearch') { + const masterKey = encrypt(generatePassword(32)); + await prisma.service.update({ + where: { id }, + data: { + type, + meiliSearch: { create: { masterKey } } + } + }); } } export async function setServiceVersion({ id, version }) { @@ -191,6 +203,9 @@ export async function updateService({ id, fqdn, name }) { export async function updateLanguageToolService({ id, fqdn, name }) { return await prisma.service.update({ where: { id }, data: { fqdn, name } }); } +export async function updateMeiliSearchService({ id, fqdn, name }) { + return await prisma.service.update({ where: { id }, data: { fqdn, name } }); +} export async function updateVaultWardenService({ id, fqdn, name }) { return await prisma.service.update({ where: { id }, data: { fqdn, name } }); } @@ -214,6 +229,7 @@ export async function updateGhostService({ id, fqdn, name, mariadbDatabase }) { } export async function removeService({ id }) { + await prisma.meiliSearch.deleteMany({ where: { serviceId: id } }); await prisma.ghost.deleteMany({ where: { serviceId: id } }); await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } }); await prisma.minio.deleteMany({ where: { serviceId: id } }); diff --git a/src/lib/docker.ts b/src/lib/docker.ts index 89290fba2..04f68ac48 100644 --- a/src/lib/docker.ts +++ b/src/lib/docker.ts @@ -88,12 +88,12 @@ export async function buildImage({ debug = false }) { if (isCache) { - saveBuildLog({ line: `Building cache image started.`, buildId, applicationId }); + await saveBuildLog({ line: `Building cache image started.`, buildId, applicationId }); } else { - saveBuildLog({ line: `Building image started.`, buildId, applicationId }); + await saveBuildLog({ line: `Building image started.`, buildId, applicationId }); } if (!debug && isCache) { - saveBuildLog({ + await saveBuildLog({ line: `Debug turned off. To see more details, allow it in the configuration.`, buildId, applicationId @@ -126,13 +126,17 @@ export async function streamEvents({ stream, docker, buildId, applicationId, deb if (err) reject(err); resolve(res); } - function onProgress(event) { + async function onProgress(event) { if (event.error) { reject(event.error); } else if (event.stream) { if (event.stream !== '\n') { if (debug) - saveBuildLog({ line: `${event.stream.replace('\n', '')}`, buildId, applicationId }); + await saveBuildLog({ + line: `${event.stream.replace('\n', '')}`, + buildId, + applicationId + }); } } } diff --git a/src/lib/haproxy/configuration.ts b/src/lib/haproxy/configuration.ts index 120e2d4cd..a7f223e0e 100644 --- a/src/lib/haproxy/configuration.ts +++ b/src/lib/haproxy/configuration.ts @@ -175,7 +175,7 @@ export async function configureHAProxy() { isRunning, isHttps, redirectValue, - redirectTo: isWWW ? domain : 'www.' + domain, + redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain, updatedAt: updatedAt.getTime() }); } @@ -199,7 +199,7 @@ export async function configureHAProxy() { isRunning, isHttps, redirectValue, - redirectTo: isWWW ? previewDomain : 'www.' + previewDomain, + redirectTo: isWWW ? previewDomain.replace('www.', '') : 'www.' + previewDomain, updatedAt: updatedAt.getTime() }); } @@ -242,7 +242,7 @@ export async function configureHAProxy() { isRunning, isHttps, redirectValue, - redirectTo: isWWW ? domain : 'www.' + domain, + redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain, updatedAt: updatedAt.getTime() }); } @@ -262,7 +262,7 @@ export async function configureHAProxy() { domain, isHttps, redirectValue, - redirectTo: isWWW ? domain : 'www.' + domain + redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain }); } const output = mustache.render(template, data); diff --git a/src/lib/importers/github.ts b/src/lib/importers/github.ts index e9bea3d42..1ab3ca90a 100644 --- a/src/lib/importers/github.ts +++ b/src/lib/importers/github.ts @@ -10,11 +10,14 @@ export default async function ({ workdir, githubAppId, repository, + apiUrl, + htmlUrl, branch, buildId }): Promise { try { - saveBuildLog({ line: 'GitHub importer started.', buildId, applicationId }); + const url = htmlUrl.replace('https://', '').replace('http://', ''); + await saveBuildLog({ line: 'GitHub importer started.', buildId, applicationId }); const { privateKey, appId, installationId } = await db.getUniqueGithubApp({ githubAppId }); const githubPrivateKey = privateKey.replace(/\\n/g, '\n').replace(/"/g, ''); @@ -27,20 +30,20 @@ export default async function ({ algorithm: 'RS256' }); const { token } = await got - .post(`https://api.github.com/app/installations/${installationId}/access_tokens`, { + .post(`${apiUrl}/app/installations/${installationId}/access_tokens`, { headers: { Authorization: `Bearer ${jwtToken}`, Accept: 'application/vnd.github.machine-man-preview+json' } }) .json(); - saveBuildLog({ + await saveBuildLog({ line: `Cloning ${repository}:${branch} branch.`, buildId, applicationId }); await asyncExecShell( - `git clone -q -b ${branch} https://x-access-token:${token}@github.com/${repository}.git ${workdir}/ && cd ${workdir} && git submodule update --init --recursive && cd ..` + `git clone -q -b ${branch} https://x-access-token:${token}@${url}/${repository}.git ${workdir}/ && cd ${workdir} && git submodule update --init --recursive && cd ..` ); const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`); return commit.replace('\n', ''); diff --git a/src/lib/importers/gitlab.ts b/src/lib/importers/gitlab.ts index fceda7fe7..07bbf7954 100644 --- a/src/lib/importers/gitlab.ts +++ b/src/lib/importers/gitlab.ts @@ -1,28 +1,28 @@ import { asyncExecShell, saveBuildLog } from '$lib/common'; -import { ErrorHandler } from '$lib/database'; export default async function ({ applicationId, - debug, workdir, repodir, + htmlUrl, repository, branch, buildId, privateSshKey }): Promise { - saveBuildLog({ line: 'GitLab importer started.', buildId, applicationId }); + const url = htmlUrl.replace('https://', '').replace('http://', ''); + await saveBuildLog({ line: 'GitLab importer started.', buildId, applicationId }); await asyncExecShell(`echo '${privateSshKey}' > ${repodir}/id.rsa`); await asyncExecShell(`chmod 600 ${repodir}/id.rsa`); - saveBuildLog({ + await saveBuildLog({ line: `Cloning ${repository}:${branch} branch.`, buildId, applicationId }); await asyncExecShell( - `git clone -q -b ${branch} git@gitlab.com:${repository}.git --config core.sshCommand="ssh -q -i ${repodir}id.rsa -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git submodule update --init --recursive && cd ..` + `git clone -q -b ${branch} git@${url}:${repository}.git --config core.sshCommand="ssh -q -i ${repodir}id.rsa -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git submodule update --init --recursive && cd ..` ); const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`); return commit.replace('\n', ''); diff --git a/src/lib/queues/builder.ts b/src/lib/queues/builder.ts index 3898bcdb5..0ef207f2a 100644 --- a/src/lib/queues/builder.ts +++ b/src/lib/queues/builder.ts @@ -51,7 +51,10 @@ export default async function (job) { pullmergeRequestId = null, sourceBranch = null, settings, - persistentStorage + persistentStorage, + pythonWSGI, + pythonModule, + pythonVariable } = job.data; const { debug } = settings; @@ -114,6 +117,7 @@ export default async function (job) { branch, buildId, apiUrl: gitSource.apiUrl, + htmlUrl: gitSource.htmlUrl, projectId, deployKeyId: gitSource.gitlabApp?.deployKeyId || null, privateSshKey: decrypt(gitSource.gitlabApp?.privateSshKey) || null @@ -127,7 +131,7 @@ export default async function (job) { } try { - db.prisma.build.update({ where: { id: buildId }, data: { commit } }); + await db.prisma.build.update({ where: { id: buildId }, data: { commit } }); } catch (err) { console.log(err); } @@ -157,7 +161,7 @@ export default async function (job) { }); deployNeeded = true; if (configHash) { - saveBuildLog({ line: 'Configuration changed.', buildId, applicationId }); + await saveBuildLog({ line: 'Configuration changed.', buildId, applicationId }); } } else { deployNeeded = false; @@ -200,16 +204,19 @@ export default async function (job) { startCommand, baseDirectory, secrets, - phpModules + phpModules, + pythonWSGI, + pythonModule, + pythonVariable }); else { - saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId }); + await saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId }); throw new Error(`Build pack ${buildPack} not found.`); } deployNeeded = true; } else { deployNeeded = false; - saveBuildLog({ line: 'Nothing changed.', buildId, applicationId }); + await saveBuildLog({ line: 'Nothing changed.', buildId, applicationId }); } // Deploy to Docker Engine @@ -259,15 +266,7 @@ export default async function (job) { // } try { - saveBuildLog({ line: 'Deployment started.', buildId, applicationId }); - // for await (const volume of volumes) { - // const id = volume.split(':')[0]; - // try { - // await asyncExecShell(`DOCKER_HOST=${host} docker volume inspect ${id}`); - // } catch (error) { - // await asyncExecShell(`DOCKER_HOST=${host} docker volume create ${id}`); - // } - // } + await saveBuildLog({ line: 'Deployment started.', buildId, applicationId }); const composeVolumes = volumes.map((volume) => { return { [`${volume.split(':')[0]}`]: { @@ -300,19 +299,12 @@ export default async function (job) { await asyncExecShell( `DOCKER_HOST=${host} docker compose --project-directory ${workdir} up -d` ); - - // const { stderr } = await asyncExecShell( - // `DOCKER_HOST=${host} docker run ${envFound && `--env-file=${workdir}/.env`} ${labels.join( - // ' ' - // )} --name ${imageId} --network ${docker.network} --restart always ${volumes.length > 0 ? volumes : '' - // } -d ${applicationId}:${tag}` - // ); - saveBuildLog({ line: 'Deployment successful!', buildId, applicationId }); + await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId }); } catch (error) { - saveBuildLog({ line: error, buildId, applicationId }); + await saveBuildLog({ line: error, buildId, applicationId }); sentry.captureException(error); throw new Error(error); } - saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId }); + await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId }); } } diff --git a/src/lib/queues/index.ts b/src/lib/queues/index.ts index 55d7ca51b..0f7dd5815 100644 --- a/src/lib/queues/index.ts +++ b/src/lib/queues/index.ts @@ -135,12 +135,12 @@ buildWorker.on('failed', async (job: Bullmq.Job, failedReason) => { const workdir = `/tmp/build-sources/${job.data.repository}`; if (!dev) await asyncExecShell(`rm -fr ${workdir}`); } - saveBuildLog({ + await saveBuildLog({ line: 'Failed to deploy!', buildId: job.data.build_id, applicationId: job.data.id }); - saveBuildLog({ + await saveBuildLog({ line: `Reason: ${failedReason.toString()}`, buildId: job.data.build_id, applicationId: job.data.id diff --git a/src/routes/applications/[id]/__layout.svelte b/src/routes/applications/[id]/__layout.svelte index 342ad99b4..f428d8d51 100644 --- a/src/routes/applications/[id]/__layout.svelte +++ b/src/routes/applications/[id]/__layout.svelte @@ -89,7 +89,6 @@ try { const { buildId } = await post(`/applications/${id}/deploy.json`, { ...application }); toast.push($t('application.deployment_queued')); - console.log($page.url); if ($page.url.pathname.startsWith(`/applications/${id}/logs/build`)) { return window.location.assign(`/applications/${id}/logs/build?buildId=${buildId}`); } else { diff --git a/src/routes/applications/[id]/index.json.ts b/src/routes/applications/[id]/index.json.ts index 3b4405706..8a67242d2 100644 --- a/src/routes/applications/[id]/index.json.ts +++ b/src/routes/applications/[id]/index.json.ts @@ -5,6 +5,7 @@ import { checkContainer } from '$lib/haproxy'; import type { RequestHandler } from '@sveltejs/kit'; import jsonwebtoken from 'jsonwebtoken'; import { get as getRequest } from '$lib/api'; +import { setDefaultConfiguration } from '$lib/buildPacks/common'; export const get: RequestHandler = async (event) => { const { teamId, status, body } = await getUserDetails(event); @@ -52,12 +53,23 @@ export const post: RequestHandler = async (event) => { buildCommand, startCommand, baseDirectory, - publishDirectory + publishDirectory, + pythonWSGI, + pythonModule, + pythonVariable } = await event.request.json(); - if (port) port = Number(port); try { + const defaultConfiguration = await setDefaultConfiguration({ + buildPack, + port, + installCommand, + startCommand, + buildCommand, + publishDirectory, + baseDirectory + }); await db.configureApplication({ id, buildPack, @@ -68,7 +80,11 @@ export const post: RequestHandler = async (event) => { buildCommand, startCommand, baseDirectory, - publishDirectory + publishDirectory, + pythonWSGI, + pythonModule, + pythonVariable, + ...defaultConfiguration }); return { status: 201 }; } catch (error) { diff --git a/src/routes/applications/[id]/index.svelte b/src/routes/applications/[id]/index.svelte index 6f9554980..04c163a43 100644 --- a/src/routes/applications/[id]/index.svelte +++ b/src/routes/applications/[id]/index.svelte @@ -38,6 +38,7 @@ import { page, session } from '$app/stores'; import { errorNotification } from '$lib/form'; import { onMount } from 'svelte'; + import Select from 'svelte-select'; import Explainer from '$lib/components/Explainer.svelte'; import Setting from '$lib/components/Setting.svelte'; @@ -58,6 +59,23 @@ let previews = application.settings.previews; let dualCerts = application.settings.dualCerts; 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) { application.fqdn = `http://${cuid()}.demo.coolify.io`; } @@ -112,7 +130,7 @@ await post(`/applications/${id}.json`, { ...application }); return window.location.reload(); } catch ({ error }) { - if (error.startsWith($t('application.dns_not_set_partial_error'))) { + if (error?.startsWith($t('application.dns_not_set_partial_error'))) { forceSave = true; } return errorNotification(error); @@ -120,12 +138,19 @@ loading = false; } } + async function selectWSGI(event) { + application.pythonWSGI = event.detail.value; + }
- + {#if application.buildPack === 'python'} +
+ +
+ +
+ {#if application.pythonWSGI?.toLowerCase() === 'gunicorn'} +
+ + +
+ {/if} + {/if} {#if !staticDeployments.includes(application.buildPack)}
@@ -336,7 +394,7 @@ name="port" id="port" bind:value={application.port} - placeholder="{$t('forms.default')}: 3000" + placeholder="{$t('forms.default')}: 'python' ? '8000' : '3000'" />
{/if} diff --git a/src/routes/applications/[id]/logs/build/index.svelte b/src/routes/applications/[id]/logs/build/index.svelte index 712c37346..a9fc68099 100644 --- a/src/routes/applications/[id]/logs/build/index.svelte +++ b/src/routes/applications/[id]/logs/build/index.svelte @@ -21,7 +21,7 @@ -
-
- {$t('application.build.build_logs_of')} - {getDomain(application.fqdn)} +
+
+
$t('application.build_logs')}
+ {application.name}
+ + {#if application.fqdn} + + + + + + + {/if} + + {#if application.gitSource?.type === 'gitlab'} + + + + {:else if application.gitSource?.type === 'github'} + + + + {/if} +
@@ -134,11 +198,14 @@
{/each}
-
- -
+ {#if !noMoreBuilds} + {#if buildCount > 5} +
+ +
+ {/if} + {/if}
{#if buildId} diff --git a/src/routes/applications/[id]/logs/index.svelte b/src/routes/applications/[id]/logs/index.svelte index c5eb52a2f..874e77242 100644 --- a/src/routes/applications/[id]/logs/index.svelte +++ b/src/routes/applications/[id]/logs/index.svelte @@ -69,16 +69,83 @@ } -
-
- Application logs of {getDomain(application.fqdn)} +
{#if logs.length === 0}
{$t('application.build.waiting_logs')}
{:else} -
+
diff --git a/src/routes/applications/[id]/previews/index.svelte b/src/routes/applications/[id]/previews/index.svelte index 5d249412d..152aa00d2 100644 --- a/src/routes/applications/[id]/previews/index.svelte +++ b/src/routes/applications/[id]/previews/index.svelte @@ -11,7 +11,6 @@ } }; } - return { status: res.status, error: new Error(`Could not load ${endpoint}`) @@ -51,14 +50,88 @@ } -
-
- Previews for {getDomain(application.fqdn)} +
+
+
+ Preview Deployments +
+ {application.name}
-
-{#if applicationSecrets.length !== 0} - +
+
+ Useful for creating staging environments." + : "These values overwrite application secrets in PR/MR deployments.
Useful for creating staging environments."} + /> +
+ {#if applicationSecrets.length !== 0} @@ -87,16 +160,9 @@ {/each}
-
-{/if} -
- + {/if}
+
{#if containers.length > 0} diff --git a/src/routes/applications/[id]/secrets/_BatchSecrets.svelte b/src/routes/applications/[id]/secrets/_BatchSecrets.svelte new file mode 100644 index 000000000..38af130c9 --- /dev/null +++ b/src/routes/applications/[id]/secrets/_BatchSecrets.svelte @@ -0,0 +1,48 @@ + + +

Paste .env file

+
+