diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile index 62de37209..04d811629 100644 --- a/.gitpod.Dockerfile +++ b/.gitpod.Dockerfile @@ -1,2 +1,2 @@ FROM gitpod/workspace-node:2022-06-20-19-54-55 -RUN (curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.27.0/pack-v0.27.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack) \ No newline at end of file +RUN brew install buildpacks/tap/pack \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 778d46764..fea6b8624 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,7 +30,8 @@ RUN mkdir -p ~/.docker/cli-plugins/ # https://download.docker.com/linux/static/stable/ RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-20.10.9 -o /usr/bin/docker # https://github.com/docker/compose/releases -RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-compose-linux-2.7.0 -o ~/.docker/cli-plugins/docker-compose +# Reverted to 2.6.1 because of this https://github.com/docker/compose/issues/9704. 2.9.0 still has a bug. +RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-compose-linux-2.6.1 -o ~/.docker/cli-plugins/docker-compose RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker RUN (curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.27.0/pack-v0.27.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack) diff --git a/apps/api/prisma/migrations/20220817082342_custom_dns_servers/migration.sql b/apps/api/prisma/migrations/20220817082342_custom_dns_servers/migration.sql new file mode 100644 index 000000000..03588b549 --- /dev/null +++ b/apps/api/prisma/migrations/20220817082342_custom_dns_servers/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Setting" ADD COLUMN "DNSServers" TEXT; diff --git a/apps/api/prisma/migrations/20220818093615_public_repositories/migration.sql b/apps/api/prisma/migrations/20220818093615_public_repositories/migration.sql new file mode 100644 index 000000000..7f08c29fd --- /dev/null +++ b/apps/api/prisma/migrations/20220818093615_public_repositories/migration.sql @@ -0,0 +1,42 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_GitSource" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "forPublic" BOOLEAN NOT NULL DEFAULT false, + "type" TEXT, + "apiUrl" TEXT, + "htmlUrl" TEXT, + "customPort" INTEGER NOT NULL DEFAULT 22, + "organization" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "githubAppId" TEXT, + "gitlabAppId" TEXT, + CONSTRAINT "GitSource_githubAppId_fkey" FOREIGN KEY ("githubAppId") REFERENCES "GithubApp" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "GitSource_gitlabAppId_fkey" FOREIGN KEY ("gitlabAppId") REFERENCES "GitlabApp" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); +INSERT INTO "new_GitSource" ("apiUrl", "createdAt", "customPort", "githubAppId", "gitlabAppId", "htmlUrl", "id", "name", "organization", "type", "updatedAt") SELECT "apiUrl", "createdAt", "customPort", "githubAppId", "gitlabAppId", "htmlUrl", "id", "name", "organization", "type", "updatedAt" FROM "GitSource"; +DROP TABLE "GitSource"; +ALTER TABLE "new_GitSource" RENAME TO "GitSource"; +CREATE UNIQUE INDEX "GitSource_githubAppId_key" ON "GitSource"("githubAppId"); +CREATE UNIQUE INDEX "GitSource_gitlabAppId_key" ON "GitSource"("gitlabAppId"); +CREATE TABLE "new_ApplicationSettings" ( + "id" TEXT NOT NULL PRIMARY KEY, + "applicationId" TEXT NOT NULL, + "dualCerts" BOOLEAN NOT NULL DEFAULT false, + "debug" BOOLEAN NOT NULL DEFAULT false, + "previews" BOOLEAN NOT NULL DEFAULT false, + "autodeploy" BOOLEAN NOT NULL DEFAULT true, + "isBot" BOOLEAN NOT NULL DEFAULT false, + "isPublicRepository" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ApplicationSettings_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_ApplicationSettings" ("applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "previews", "updatedAt") SELECT "applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "previews", "updatedAt" FROM "ApplicationSettings"; +DROP TABLE "ApplicationSettings"; +ALTER TABLE "new_ApplicationSettings" RENAME TO "ApplicationSettings"; +CREATE UNIQUE INDEX "ApplicationSettings_applicationId_key" ON "ApplicationSettings"("applicationId"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index b48d5c52c..d4a7ce2f5 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -20,6 +20,7 @@ model Setting { proxyHash String? isAutoUpdateEnabled Boolean @default(false) isDNSCheckEnabled Boolean @default(true) + DNSServers String? isTraefikUsed Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -118,16 +119,17 @@ model Application { } model ApplicationSettings { - id String @id @default(cuid()) - applicationId String @unique - dualCerts Boolean @default(false) - debug Boolean @default(false) - previews Boolean @default(false) - autodeploy Boolean @default(true) - isBot Boolean @default(false) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - application Application @relation(fields: [applicationId], references: [id]) + id String @id @default(cuid()) + applicationId String @unique + dualCerts Boolean @default(false) + debug Boolean @default(false) + previews Boolean @default(false) + autodeploy Boolean @default(true) + isBot Boolean @default(false) + isPublicRepository Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + application Application @relation(fields: [applicationId], references: [id]) } model ApplicationPersistentStorage { @@ -237,6 +239,7 @@ model SshKey { model GitSource { id String @id @default(cuid()) name String + forPublic Boolean @default(false) type String? apiUrl String? htmlUrl String? diff --git a/apps/api/prisma/seed.js b/apps/api/prisma/seed.js index 96b55b105..9fa55136f 100644 --- a/apps/api/prisma/seed.js +++ b/apps/api/prisma/seed.js @@ -66,6 +66,34 @@ async function main() { } }); } + const github = await prisma.gitSource.findFirst({ + where: { htmlUrl: 'https://github.com', forPublic: true } + }); + const gitlab = await prisma.gitSource.findFirst({ + where: { htmlUrl: 'https://gitlab.com', forPublic: true } + }); + if (!github) { + await prisma.gitSource.create({ + data: { + apiUrl: 'https://api.github.com', + htmlUrl: 'https://github.com', + forPublic: true, + name: 'Github Public', + type: 'github' + } + }); + } + if (!gitlab) { + await prisma.gitSource.create({ + data: { + apiUrl: 'https://gitlab.com/api/v4', + htmlUrl: 'https://gitlab.com', + forPublic: true, + name: 'Gitlab Public', + type: 'gitlab' + } + }); + } } main() .catch((e) => { diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index ed862a59e..18864dd6e 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -5,8 +5,10 @@ import env from '@fastify/env'; import cookie from '@fastify/cookie'; import path, { join } from 'path'; import autoLoad from '@fastify/autoload'; -import { asyncExecShell, isDev, listSettings, prisma } from './lib/common'; +import { asyncExecShell, isDev, listSettings, prisma, version } from './lib/common'; import { scheduler } from './lib/scheduler'; +import axios from 'axios'; +import compareVersions from 'compare-versions'; declare module 'fastify' { interface FastifyInstance { @@ -113,8 +115,22 @@ fastify.listen({ port, host }, async (err: any, address: any) => { setInterval(async () => { const { isAutoUpdateEnabled } = await prisma.setting.findFirst(); if (isAutoUpdateEnabled) { - if (scheduler.workers.has('deployApplication')) { - scheduler.workers.get('deployApplication').postMessage("status:autoUpdater"); + const currentVersion = version; + const { data: versions } = await axios + .get( + `https://get.coollabs.io/versions.json` + , { + params: { + appId: process.env['COOLIFY_APP_ID'] || undefined, + version: currentVersion + } + }) + const latestVersion = versions['coolify'].main.version; + const isUpdateAvailable = compareVersions(latestVersion, currentVersion); + if (isUpdateAvailable === 1) { + if (scheduler.workers.has('deployApplication')) { + scheduler.workers.get('deployApplication').postMessage("status:autoUpdater"); + } } } }, isDev ? 5000 : 60000 * 15) diff --git a/apps/api/src/jobs/deployApplication.ts b/apps/api/src/jobs/deployApplication.ts index a8c46d594..9259e3976 100644 --- a/apps/api/src/jobs/deployApplication.ts +++ b/apps/api/src/jobs/deployApplication.ts @@ -4,7 +4,7 @@ import fs from 'fs/promises'; import yaml from 'js-yaml'; import { copyBaseConfigurationFiles, makeLabelForStandaloneApplication, saveBuildLog, setDefaultConfiguration } from '../lib/buildPacks/common'; -import { createDirectories, decrypt, executeDockerCmd, getDomain, prisma } from '../lib/common'; +import { createDirectories, decrypt, defaultComposeConfiguration, executeDockerCmd, getDomain, prisma } from '../lib/common'; import * as importers from '../lib/importers'; import * as buildpacks from '../lib/buildPacks'; @@ -56,6 +56,7 @@ import * as buildpacks from '../lib/buildPacks'; baseImage, baseBuildImage, deploymentType, + forceRebuild } = message let { branch, @@ -69,6 +70,30 @@ import * as buildpacks from '../lib/buildPacks'; dockerFileLocation, denoMainFile } = message + const currentHash = crypto + .createHash('sha256') + .update( + JSON.stringify({ + pythonWSGI, + pythonModule, + pythonVariable, + deploymentType, + denoOptions, + baseImage, + baseBuildImage, + buildPack, + port, + exposePort, + installCommand, + buildCommand, + startCommand, + secrets, + branch, + repository, + fqdn + }) + ) + .digest('hex'); try { const { debug } = settings; if (concurrency === 1) { @@ -131,7 +156,8 @@ import * as buildpacks from '../lib/buildPacks'; htmlUrl: gitSource.htmlUrl, projectId, deployKeyId: gitSource.gitlabApp?.deployKeyId || null, - privateSshKey: decrypt(gitSource.gitlabApp?.privateSshKey) || null + privateSshKey: decrypt(gitSource.gitlabApp?.privateSshKey) || null, + forPublic: gitSource.forPublic }); if (!commit) { throw new Error('No commit found?'); @@ -146,38 +172,10 @@ import * as buildpacks from '../lib/buildPacks'; } catch (err) { console.log(err); } + if (!pullmergeRequestId) { - const currentHash = crypto - //@ts-ignore - .createHash('sha256') - .update( - JSON.stringify({ - pythonWSGI, - pythonModule, - pythonVariable, - deploymentType, - denoOptions, - baseImage, - baseBuildImage, - buildPack, - port, - exposePort, - installCommand, - buildCommand, - startCommand, - secrets, - branch, - repository, - fqdn - }) - ) - .digest('hex'); if (configHash !== currentHash) { - await prisma.application.update({ - where: { id: applicationId }, - data: { configHash: currentHash } - }); deployNeeded = true; if (configHash) { await saveBuildLog({ line: 'Configuration changed.', buildId, applicationId }); @@ -200,8 +198,10 @@ import * as buildpacks from '../lib/buildPacks'; // } await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId, baseImage); + + if (forceRebuild) deployNeeded = true if (!imageFound || deployNeeded) { - // if (true) { + // if (true) { if (buildpacks[buildPack]) await buildpacks[buildPack]({ dockerId: destinationDocker.id, @@ -250,16 +250,18 @@ import * as buildpacks from '../lib/buildPacks'; } catch (error) { // } - const envs = []; + const envs = [ + `PORT=${port}` + ]; if (secrets.length > 0) { secrets.forEach((secret) => { if (pullmergeRequestId) { if (secret.isPRMRSecret) { - envs.push(`${secret.name}='${secret.value}'`); + envs.push(`${secret.name}=${secret.value}`); } } else { if (!secret.isPRMRSecret) { - envs.push(`${secret.name}='${secret.value}'`); + envs.push(`${secret.name}=${secret.value}`); } } }); @@ -306,23 +308,14 @@ import * as buildpacks from '../lib/buildPacks'; container_name: imageId, volumes, env_file: envFound ? [`${workdir}/.env`] : [], - networks: [destinationDocker.network], labels, depends_on: [], - restart: 'always', expose: [port], ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), // logging: { // driver: 'fluentd', // }, - deploy: { - restart_policy: { - condition: 'on-failure', - delay: '5s', - max_attempts: 3, - window: '120s' - } - } + ...defaultComposeConfiguration(destinationDocker.network), } }, networks: { @@ -345,6 +338,10 @@ import * as buildpacks from '../lib/buildPacks'; } await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId }); await prisma.build.update({ where: { id: message.build_id }, data: { status: 'success' } }); + if (!pullmergeRequestId) await prisma.application.update({ + where: { id: applicationId }, + data: { configHash: currentHash } + }); } } diff --git a/apps/api/src/lib/common.ts b/apps/api/src/lib/common.ts index a2eb41dae..50e41a1fd 100644 --- a/apps/api/src/lib/common.ts +++ b/apps/api/src/lib/common.ts @@ -17,7 +17,7 @@ import { checkContainer, removeContainer } from './docker'; import { day } from './dayjs'; import * as serviceFields from './serviceFields' -export const version = '3.5.0'; +export const version = '3.6.0'; export const isDev = process.env.NODE_ENV === 'development'; const algorithm = 'aes-256-ctr'; @@ -319,6 +319,10 @@ export async function checkDoubleBranch(branch: string, projectId: number): Prom } export async function isDNSValid(hostname: any, domain: string): Promise { const { isIP } = await import('is-ip'); + const { DNSServers } = await listSettings(); + if (DNSServers) { + dns.setServers([DNSServers]); + } let resolves = []; try { if (isIP(hostname)) { @@ -332,7 +336,6 @@ export async function isDNSValid(hostname: any, domain: string): Promise { try { let ipDomainFound = false; - dns.setServers(['1.1.1.1', '8.8.8.8']); const dnsResolve = await dns.resolve4(domain); if (dnsResolve.length > 0) { for (const ip of dnsResolve) { @@ -424,7 +427,12 @@ export async function checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts }): P const { isIP } = await import('is-ip'); const domain = getDomain(fqdn); const domainDualCert = domain.includes('www.') ? domain.replace('www.', '') : `www.${domain}`; - dns.setServers(['1.1.1.1', '8.8.8.8']); + + const { DNSServers } = await listSettings(); + if (DNSServers) { + dns.setServers([DNSServers]); + } + let resolves = []; try { if (isIP(hostname)) { @@ -1180,6 +1188,25 @@ export async function updatePasswordInDb(database, user, newPassword, isRoot) { } } } +export async function checkExposedPort({ id, configuredPort, exposePort, dockerId, remoteIpAddress }: { id: string, configuredPort?: number, exposePort: number, dockerId: string, remoteIpAddress?: string }) { + if (exposePort < 1024 || exposePort > 65535) { + throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` } + } + + if (configuredPort) { + if (configuredPort !== exposePort) { + const availablePort = await getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress); + if (availablePort.toString() !== exposePort.toString()) { + throw { status: 500, message: `Port ${exposePort} is already in use.` } + } + } + } else { + const availablePort = await getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress); + if (availablePort.toString() !== exposePort.toString()) { + throw { status: 500, message: `Port ${exposePort} is already in use.` } + } + } +} export async function getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress) { const { default: getPort } = await import('get-port'); const applicationUsed = await ( @@ -1565,7 +1592,7 @@ export async function configureServiceType({ }); } else if (type === 'appwrite') { const opensslKeyV1 = encrypt(generatePassword()); - const executorSecret = encrypt(generatePassword()); + const executorSecret = encrypt(generatePassword()); const redisPassword = encrypt(generatePassword()); const mariadbHost = `${id}-mariadb` const mariadbUser = cuid(); @@ -1845,3 +1872,17 @@ export function persistentVolumes(id, persistentStorage, config) { ) || {} return { volumes, volumeMounts } } +export function defaultComposeConfiguration(network: string): any { + return { + networks: [network], + restart: 'on-failure', + deploy: { + restart_policy: { + condition: 'on-failure', + delay: '5s', + max_attempts: 10, + window: '120s' + } + } + } +} diff --git a/apps/api/src/lib/docker.ts b/apps/api/src/lib/docker.ts index aa6a29cfd..c1ae977e5 100644 --- a/apps/api/src/lib/docker.ts +++ b/apps/api/src/lib/docker.ts @@ -71,7 +71,6 @@ export async function removeContainer({ }): Promise { try { const { stdout } = await executeDockerCmd({ dockerId, command: `docker inspect --format '{{json .State}}' ${id}` }) - console.log(id) if (JSON.parse(stdout).Running) { await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` }) await executeDockerCmd({ dockerId, command: `docker rm ${id}` }) diff --git a/apps/api/src/lib/importers/github.ts b/apps/api/src/lib/importers/github.ts index bac460eca..798931d7a 100644 --- a/apps/api/src/lib/importers/github.ts +++ b/apps/api/src/lib/importers/github.ts @@ -12,7 +12,8 @@ export default async function ({ htmlUrl, branch, buildId, - customPort + customPort, + forPublic }: { applicationId: string; workdir: string; @@ -23,41 +24,55 @@ export default async function ({ branch: string; buildId: string; customPort: number; + forPublic?: boolean; }): Promise { const { default: got } = await import('got') const url = htmlUrl.replace('https://', '').replace('http://', ''); await saveBuildLog({ line: 'GitHub importer started.', buildId, applicationId }); + if (forPublic) { + await saveBuildLog({ + line: `Cloning ${repository}:${branch} branch.`, + buildId, + applicationId + }); + await asyncExecShell( + `git clone -q -b ${branch} https://${url}/${repository}.git ${workdir}/ && cd ${workdir} && git submodule update --init --recursive && git lfs pull && cd .. ` + ); - const body = await prisma.githubApp.findUnique({ where: { id: githubAppId } }); - if (body.privateKey) body.privateKey = decrypt(body.privateKey); - const { privateKey, appId, installationId } = body + } else { + const body = await prisma.githubApp.findUnique({ where: { id: githubAppId } }); + if (body.privateKey) body.privateKey = decrypt(body.privateKey); + const { privateKey, appId, installationId } = body + const githubPrivateKey = privateKey.replace(/\\n/g, '\n').replace(/"/g, ''); - const githubPrivateKey = privateKey.replace(/\\n/g, '\n').replace(/"/g, ''); - - const payload = { - iat: Math.round(new Date().getTime() / 1000), - exp: Math.round(new Date().getTime() / 1000 + 60), - iss: appId - }; - const jwtToken = jsonwebtoken.sign(payload, githubPrivateKey, { - algorithm: 'RS256' - }); - const { token } = await got - .post(`${apiUrl}/app/installations/${installationId}/access_tokens`, { - headers: { - Authorization: `Bearer ${jwtToken}`, - Accept: 'application/vnd.github.machine-man-preview+json' - } - }) - .json(); - await saveBuildLog({ - line: `Cloning ${repository}:${branch} branch.`, - buildId, - applicationId - }); - await asyncExecShell( - `git clone -q -b ${branch} https://x-access-token:${token}@${url}/${repository}.git --config core.sshCommand="ssh -p ${customPort}" ${workdir}/ && cd ${workdir} && git submodule update --init --recursive && git lfs pull && cd .. ` - ); + const payload = { + iat: Math.round(new Date().getTime() / 1000), + exp: Math.round(new Date().getTime() / 1000 + 60), + iss: appId + }; + const jwtToken = jsonwebtoken.sign(payload, githubPrivateKey, { + algorithm: 'RS256' + }); + const { token } = await got + .post(`${apiUrl}/app/installations/${installationId}/access_tokens`, { + headers: { + Authorization: `Bearer ${jwtToken}`, + Accept: 'application/vnd.github.machine-man-preview+json' + } + }) + .json(); + await saveBuildLog({ + line: `Cloning ${repository}:${branch} branch.`, + buildId, + applicationId + }); + await asyncExecShell( + `git clone -q -b ${branch} https://x-access-token:${token}@${url}/${repository}.git --config core.sshCommand="ssh -p ${customPort}" ${workdir}/ && cd ${workdir} && git submodule update --init --recursive && git lfs pull && cd .. ` + ); + } const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`); + return commit.replace('\n', ''); + + } diff --git a/apps/api/src/lib/scheduler.ts b/apps/api/src/lib/scheduler.ts index 148764351..b120dcea1 100644 --- a/apps/api/src/lib/scheduler.ts +++ b/apps/api/src/lib/scheduler.ts @@ -20,7 +20,6 @@ const options: any = { } if (message.caller === 'cleanupStorage') { if (!scheduler.workers.has('cleanupStorage')) { - await scheduler.stop('deployApplication'); await scheduler.run('cleanupStorage') } } diff --git a/apps/api/src/lib/services.ts b/apps/api/src/lib/services.ts index c5d315ff0..d7f0fd75e 100644 --- a/apps/api/src/lib/services.ts +++ b/apps/api/src/lib/services.ts @@ -17,19 +17,4 @@ export async function defaultServiceConfigurations({ id, teamId }) { }); } return { ...service, network, port, workdir, image, secrets } -} - -export function defaultServiceComposeConfiguration(network: string): any { - return { - networks: [network], - restart: 'always', - deploy: { - restart_policy: { - condition: 'on-failure', - delay: '10s', - max_attempts: 10, - window: '120s' - } - } - } } \ No newline at end of file diff --git a/apps/api/src/routes/api/v1/applications/handlers.ts b/apps/api/src/routes/api/v1/applications/handlers.ts index cea2ca294..57da91478 100644 --- a/apps/api/src/routes/api/v1/applications/handlers.ts +++ b/apps/api/src/routes/api/v1/applications/handlers.ts @@ -5,7 +5,7 @@ import axios from 'axios'; import { FastifyReply } from 'fastify'; import { day } from '../../../../lib/dayjs'; import { setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common'; -import { checkDomainsIsValidInDNS, checkDoubleBranch, decrypt, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, getFreeExposedPort, isDev, isDomainConfigured, listSettings, prisma, stopBuild, uniqueName } from '../../../../lib/common'; +import { checkDomainsIsValidInDNS, checkDoubleBranch, checkExposedPort, decrypt, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, getFreeExposedPort, isDev, isDomainConfigured, listSettings, prisma, stopBuild, uniqueName } from '../../../../lib/common'; import { checkContainer, formatLabelsOnDocker, isContainerExited, removeContainer } from '../../../../lib/docker'; import { scheduler } from '../../../../lib/scheduler'; @@ -18,7 +18,7 @@ export async function listApplications(request: FastifyRequest) { const { teamId } = request.user const applications = await prisma.application.findMany({ where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, - include: { teams: true, destinationDocker: true } + include: { teams: true, destinationDocker: true, settings: true } }); const settings = await prisma.setting.findFirst() return { @@ -238,6 +238,9 @@ export async function saveApplication(request: FastifyRequest, if (exposePort) { exposePort = Number(exposePort); } + + const { destinationDockerId } = await prisma.application.findUnique({ where: { id } }) + if (exposePort) await checkExposedPort({ id, exposePort, dockerId: destinationDockerId }) if (denoOptions) denoOptions = denoOptions.trim(); const defaultConfiguration = await setDefaultConfiguration({ buildPack, @@ -392,18 +395,7 @@ export async function checkDNS(request: FastifyRequest) { if (found) { throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` } } - if (exposePort) { - if (exposePort < 1024 || exposePort > 65535) { - throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` } - } - - if (configuredPort !== exposePort) { - const availablePort = await getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress); - if (availablePort.toString() !== exposePort.toString()) { - throw { status: 500, message: `Port ${exposePort} is already in use.` } - } - } - } + await checkExposedPort({ id, configuredPort, exposePort, dockerId, remoteIpAddress }) if (isDNSCheckEnabled && !isDev && !forceSave) { let hostname = request.hostname.split(':')[0]; if (remoteEngine) hostname = remoteIpAddress; @@ -436,7 +428,7 @@ export async function deployApplication(request: FastifyRequest, reply: FastifyReply) { try { const { id } = request.params - const { gitSourceId } = request.body - await prisma.application.update({ - where: { id }, - data: { gitSource: { connect: { id: gitSourceId } } } - }); + const { gitSourceId, forPublic, type } = request.body + if (forPublic) { + const publicGit = await prisma.gitSource.findFirst({ where: { type, forPublic } }); + await prisma.application.update({ + where: { id }, + data: { gitSource: { connect: { id: publicGit.id } } } + }); + } else { + await prisma.application.update({ + where: { id }, + data: { gitSource: { connect: { id: gitSourceId } } } + }); + } + return reply.code(201).send() } catch ({ status, message }) { return errorHandler({ status, message }) @@ -557,7 +560,7 @@ export async function checkRepository(request: FastifyRequest) export async function saveRepository(request, reply) { try { const { id } = request.params - let { repository, branch, projectId, autodeploy, webhookToken } = request.body + let { repository, branch, projectId, autodeploy, webhookToken, isPublicRepository = false } = request.body repository = repository.toLowerCase(); branch = branch.toLowerCase(); @@ -565,17 +568,19 @@ export async function saveRepository(request, reply) { if (webhookToken) { await prisma.application.update({ where: { id }, - data: { repository, branch, projectId, gitSource: { update: { gitlabApp: { update: { webhookToken: webhookToken ? webhookToken : undefined } } } }, settings: { update: { autodeploy } } } + data: { repository, branch, projectId, gitSource: { update: { gitlabApp: { update: { webhookToken: webhookToken ? webhookToken : undefined } } } }, settings: { update: { autodeploy, isPublicRepository } } } }); } else { await prisma.application.update({ where: { id }, - data: { repository, branch, projectId, settings: { update: { autodeploy } } } + data: { repository, branch, projectId, settings: { update: { autodeploy, isPublicRepository } } } }); } - const isDouble = await checkDoubleBranch(branch, projectId); - if (isDouble) { - await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false } }) + if (!isPublicRepository) { + const isDouble = await checkDoubleBranch(branch, projectId); + if (isDouble) { + await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false, isPublicRepository } }) + } } return reply.code(201).send() } catch ({ status, message }) { @@ -607,7 +612,8 @@ export async function getBuildPack(request) { projectId: application.projectId, repository: application.repository, branch: application.branch, - apiUrl: application.gitSource.apiUrl + apiUrl: application.gitSource.apiUrl, + isPublicRepository: application.settings.isPublicRepository } } catch ({ status, message }) { return errorHandler({ status, message }) @@ -657,13 +663,13 @@ export async function saveSecret(request: FastifyRequest, reply: Fas if (found) { throw { status: 500, message: `Secret ${name} already exists.` } } else { - value = encrypt(value); + value = encrypt(value.trim()); await prisma.secret.create({ data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } } }); } } else { - value = encrypt(value); + value = encrypt(value.trim()); const found = await prisma.secret.findFirst({ where: { applicationId: id, name, isPRMRSecret } }); if (found) { diff --git a/apps/api/src/routes/api/v1/applications/types.ts b/apps/api/src/routes/api/v1/applications/types.ts index 1f06a37a8..6452c0c9d 100644 --- a/apps/api/src/routes/api/v1/applications/types.ts +++ b/apps/api/src/routes/api/v1/applications/types.ts @@ -44,13 +44,13 @@ export interface CheckDNS extends OnlyId { } export interface DeployApplication { Querystring: { domain: string } - Body: { pullmergeRequestId: string | null, branch: string } + Body: { pullmergeRequestId: string | null, branch: string, forceRebuild?: boolean } } export interface GetImages { Body: { buildPack: string, deploymentType: string } } export interface SaveApplicationSource extends OnlyId { - Body: { gitSourceId: string } + Body: { gitSourceId?: string | null, forPublic?: boolean, type?: string } } export interface CheckRepository extends OnlyId { Querystring: { repository: string, branch: string } @@ -115,7 +115,8 @@ export interface CancelDeployment { export interface DeployApplication extends OnlyId { Body: { pullmergeRequestId: string | null, - branch: string + branch: string, + forceRebuild?: boolean } } diff --git a/apps/api/src/routes/api/v1/destinations/handlers.ts b/apps/api/src/routes/api/v1/destinations/handlers.ts index f310f8a61..2148a1450 100644 --- a/apps/api/src/routes/api/v1/destinations/handlers.ts +++ b/apps/api/src/routes/api/v1/destinations/handlers.ts @@ -79,7 +79,6 @@ export async function newDestination(request: FastifyRequest, re let { name, network, engine, isCoolifyProxyUsed, remoteIpAddress, remoteUser, remotePort } = request.body if (id === 'new') { - console.log(engine) if (engine) { const { stdout } = await asyncExecShell(`DOCKER_HOST=unix:///var/run/docker.sock docker network ls --filter 'name=^${network}$' --format '{{json .}}'`); if (stdout === '') { diff --git a/apps/api/src/routes/api/v1/handlers.ts b/apps/api/src/routes/api/v1/handlers.ts index 6d3112e97..ae558320e 100644 --- a/apps/api/src/routes/api/v1/handlers.ts +++ b/apps/api/src/routes/api/v1/handlers.ts @@ -4,7 +4,7 @@ import axios from 'axios'; import compare from 'compare-versions'; import cuid from 'cuid'; import bcrypt from 'bcryptjs'; -import { asyncExecShell, asyncSleep, cleanupDockerStorage, errorHandler, isDev, prisma, uniqueName, version } from '../../../lib/common'; +import { asyncExecShell, asyncSleep, cleanupDockerStorage, errorHandler, isDev, listSettings, prisma, uniqueName, version } from '../../../lib/common'; import type { FastifyReply, FastifyRequest } from 'fastify'; import type { Login, Update } from '.'; @@ -97,7 +97,8 @@ export async function showDashboard(request: FastifyRequest) { const userId = request.user.userId; const teamId = request.user.teamId; const applications = await prisma.application.findMany({ - where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } } + where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, + include: { settings: true } }); const databases = await prisma.database.findMany({ where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } } @@ -105,10 +106,12 @@ export async function showDashboard(request: FastifyRequest) { const services = await prisma.service.findMany({ where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } } }); + const settings = await listSettings(); return { applications, databases, services, + settings, }; } catch ({ status, message }) { return errorHandler({ status, message }) diff --git a/apps/api/src/routes/api/v1/services/handlers.ts b/apps/api/src/routes/api/v1/services/handlers.ts index d5b5893bf..6301a61d7 100644 --- a/apps/api/src/routes/api/v1/services/handlers.ts +++ b/apps/api/src/routes/api/v1/services/handlers.ts @@ -2,14 +2,14 @@ import type { FastifyReply, FastifyRequest } from 'fastify'; import fs from 'fs/promises'; import yaml from 'js-yaml'; import bcrypt from 'bcryptjs'; -import { prisma, uniqueName, asyncExecShell, getServiceImage, configureServiceType, getServiceFromDB, getContainerUsage, removeService, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, getServiceMainPort, createDirectories, ComposeFile, makeLabelForServices, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, supportedServiceTypesAndVersions, executeDockerCmd, listSettings, getFreeExposedPort, checkDomainsIsValidInDNS, persistentVolumes, asyncSleep, isARM } from '../../../../lib/common'; +import { prisma, uniqueName, asyncExecShell, getServiceImage, configureServiceType, getServiceFromDB, getContainerUsage, removeService, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, getServiceMainPort, createDirectories, ComposeFile, makeLabelForServices, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, supportedServiceTypesAndVersions, executeDockerCmd, listSettings, getFreeExposedPort, checkDomainsIsValidInDNS, persistentVolumes, asyncSleep, isARM, defaultComposeConfiguration, checkExposedPort } from '../../../../lib/common'; import { day } from '../../../../lib/dayjs'; import { checkContainer, isContainerExited, removeContainer } from '../../../../lib/docker'; import cuid from 'cuid'; import type { OnlyId } from '../../../../types'; import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetWordpressSettings } from './types'; -import { defaultServiceComposeConfiguration, defaultServiceConfigurations } from '../../../../lib/services'; +import { defaultServiceConfigurations } from '../../../../lib/services'; // async function startServiceNew(request: FastifyRequest) { // try { @@ -378,18 +378,7 @@ export async function checkService(request: FastifyRequest) { } } } - if (exposePort) { - if (exposePort < 1024 || exposePort > 65535) { - throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` } - } - - if (configuredPort !== exposePort) { - const availablePort = await getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress); - if (availablePort.toString() !== exposePort.toString()) { - throw { status: 500, message: `Port ${exposePort} is already in use.` } - } - } - } + await checkExposedPort({ id, configuredPort, exposePort, dockerId, remoteIpAddress }) if (isDNSCheckEnabled && !isDev && !forceSave) { let hostname = request.hostname.split(':')[0]; if (remoteEngine) hostname = remoteIpAddress; @@ -458,13 +447,13 @@ export async function saveServiceSecret(request: FastifyRequest) { environment: config.environmentVariables, ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('nocodb'), - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), } }, networks: { @@ -959,7 +948,7 @@ async function startMinioService(request: FastifyRequest) { volumes, ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('minio'), - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), } }, networks: { @@ -1025,7 +1014,7 @@ async function startVscodeService(request: FastifyRequest) { volumes, ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('vscodeServer'), - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), } }, networks: { @@ -1132,7 +1121,7 @@ async function startWordpressService(request: FastifyRequest) volumes, ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('wordpress'), - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), } }, networks: { @@ -1149,7 +1138,7 @@ async function startWordpressService(request: FastifyRequest) image: config.mysql.image, volumes: [config.mysql.volume], environment: config.mysql.environmentVariables, - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), }; composeFile.volumes[config.mysql.volume.split(':')[0]] = { @@ -1202,7 +1191,7 @@ async function startVaultwardenService(request: FastifyRequest volumes, ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('vaultWarden'), - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), } }, networks: { @@ -1258,7 +1247,7 @@ async function startLanguageToolService(request: FastifyRequest) { environment: config.environmentVariables, labels: makeLabelForServices('n8n'), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), } }, networks: { @@ -1370,7 +1359,7 @@ async function startUptimekumaService(request: FastifyRequest) environment: config.environmentVariables, ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('uptimekuma'), - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), } }, networks: { @@ -1469,14 +1458,14 @@ async function startGhostService(request: FastifyRequest) { ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('ghost'), depends_on: [`${id}-mariadb`], - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), }, [`${id}-mariadb`]: { container_name: `${id}-mariadb`, image: config.mariadb.image, volumes: [config.mariadb.volume], environment: config.mariadb.environmentVariables, - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), } }, networks: { @@ -1542,7 +1531,7 @@ async function startMeilisearchService(request: FastifyRequest ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), volumes, labels: makeLabelForServices('meilisearch'), - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), } }, networks: { @@ -1708,14 +1697,14 @@ async function startUmamiService(request: FastifyRequest) { ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('umami'), depends_on: [`${id}-postgresql`], - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), }, [`${id}-postgresql`]: { build: workdir, container_name: `${id}-postgresql`, environment: config.postgresql.environmentVariables, volumes: [config.postgresql.volume], - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), } }, networks: { @@ -1795,14 +1784,14 @@ async function startHasuraService(request: FastifyRequest) { labels: makeLabelForServices('hasura'), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), depends_on: [`${id}-postgresql`], - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), }, [`${id}-postgresql`]: { image: config.postgresql.image, container_name: `${id}-postgresql`, environment: config.postgresql.environmentVariables, volumes: [config.postgresql.volume], - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), } }, networks: { @@ -1908,14 +1897,14 @@ async function startFiderService(request: FastifyRequest) { labels: makeLabelForServices('fider'), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), depends_on: [`${id}-postgresql`], - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), }, [`${id}-postgresql`]: { image: config.postgresql.image, container_name: `${id}-postgresql`, environment: config.postgresql.environmentVariables, volumes: [config.postgresql.volume], - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), } }, networks: { @@ -2001,7 +1990,7 @@ async function startAppWriteService(request: FastifyRequest) { "_APP_STATSD_PORT=8125", ...secrets ], - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), }, [`${id}-realtime`]: { image: `${image}:${version}`, @@ -2024,7 +2013,7 @@ async function startAppWriteService(request: FastifyRequest) { `_APP_DB_PASS=${mariadbPassword}`, ...secrets ], - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), }, [`${id}-worker-audits`]: { @@ -2048,7 +2037,7 @@ async function startAppWriteService(request: FastifyRequest) { `_APP_DB_PASS=${mariadbPassword}`, ...secrets ], - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), }, [`${id}-worker-webhooks`]: { image: `${image}:${version}`, @@ -2066,7 +2055,7 @@ async function startAppWriteService(request: FastifyRequest) { "_APP_REDIS_PORT=6379", ...secrets ], - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), }, [`${id}-worker-deletes`]: { image: `${image}:${version}`, @@ -2099,7 +2088,7 @@ async function startAppWriteService(request: FastifyRequest) { `_APP_EXECUTOR_HOST=http://${id}-executor/v1`, ...secrets ], - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), }, [`${id}-worker-databases`]: { image: `${image}:${version}`, @@ -2122,7 +2111,7 @@ async function startAppWriteService(request: FastifyRequest) { `_APP_DB_PASS=${mariadbPassword}`, ...secrets ], - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), }, [`${id}-worker-builds`]: { image: `${image}:${version}`, @@ -2147,7 +2136,7 @@ async function startAppWriteService(request: FastifyRequest) { `_APP_DB_PASS=${mariadbPassword}`, ...secrets ], - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), }, [`${id}-worker-certificates`]: { image: `${image}:${version}`, @@ -2176,7 +2165,7 @@ async function startAppWriteService(request: FastifyRequest) { `_APP_DB_PASS=${mariadbPassword}`, ...secrets ], - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), }, [`${id}-worker-functions`]: { image: `${image}:${version}`, @@ -2202,7 +2191,7 @@ async function startAppWriteService(request: FastifyRequest) { `_APP_EXECUTOR_HOST=http://${id}-executor/v1`, ...secrets ], - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), }, [`${id}-executor`]: { image: `${image}:${version}`, @@ -2226,7 +2215,7 @@ async function startAppWriteService(request: FastifyRequest) { `_APP_EXECUTOR_SECRET=${executorSecret}`, ...secrets ], - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), }, [`${id}-worker-mails`]: { image: `${image}:${version}`, @@ -2243,7 +2232,7 @@ async function startAppWriteService(request: FastifyRequest) { "_APP_REDIS_PORT=6379", ...secrets ], - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), }, [`${id}-worker-messaging`]: { image: `${image}:${version}`, @@ -2259,7 +2248,7 @@ async function startAppWriteService(request: FastifyRequest) { "_APP_REDIS_PORT=6379", ...secrets ], - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), }, [`${id}-maintenance`]: { image: `${image}:${version}`, @@ -2283,7 +2272,7 @@ async function startAppWriteService(request: FastifyRequest) { `_APP_DB_PASS=${mariadbPassword}`, ...secrets ], - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), }, [`${id}-schedule`]: { image: `${image}:${version}`, @@ -2299,7 +2288,7 @@ async function startAppWriteService(request: FastifyRequest) { "_APP_REDIS_PORT=6379", ...secrets ], - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), }, [`${id}-mariadb`]: { "image": "mariadb:10.7", @@ -2316,7 +2305,7 @@ async function startAppWriteService(request: FastifyRequest) { `MYSQL_DATABASE=${mariadbDatabase}` ], "command": "mysqld --innodb-flush-method=fsync", - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), }, [`${id}-redis`]: { "image": "redis:6.2-alpine", @@ -2325,7 +2314,7 @@ async function startAppWriteService(request: FastifyRequest) { "volumes": [ `${id}-redis:/data:rw` ], - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), }, }; @@ -2354,7 +2343,7 @@ async function startAppWriteService(request: FastifyRequest) { "_APP_REDIS_PORT=6379", ...secrets ], - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), } dockerCompose[`${id}-influxdb`] = { "image": "appwrite/influxdb:1.5.0", @@ -2362,7 +2351,7 @@ async function startAppWriteService(request: FastifyRequest) { "volumes": [ `${id}-influxdb:/var/lib/influxdb:rw` ], - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), } dockerCompose[`${id}-telegraf`] = { "image": "appwrite/telegraf:1.4.0", @@ -2371,7 +2360,7 @@ async function startAppWriteService(request: FastifyRequest) { `_APP_INFLUXDB_HOST=${id}-influxdb`, "_APP_INFLUXDB_PORT=8086", ], - ...defaultServiceComposeConfiguration(network), + ...defaultComposeConfiguration(network), } } diff --git a/apps/api/src/routes/api/v1/settings/handlers.ts b/apps/api/src/routes/api/v1/settings/handlers.ts index 073dbd7e3..068d2c97e 100644 --- a/apps/api/src/routes/api/v1/settings/handlers.ts +++ b/apps/api/src/routes/api/v1/settings/handlers.ts @@ -33,12 +33,13 @@ export async function saveSettings(request: FastifyRequest, reply: minPort, maxPort, isAutoUpdateEnabled, - isDNSCheckEnabled + isDNSCheckEnabled, + DNSServers } = request.body const { id } = await listSettings(); await prisma.setting.update({ where: { id }, - data: { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled, isDNSCheckEnabled } + data: { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled, isDNSCheckEnabled, DNSServers } }); if (fqdn) { await prisma.setting.update({ where: { id }, data: { fqdn } }); @@ -54,6 +55,10 @@ export async function saveSettings(request: FastifyRequest, reply: export async function deleteDomain(request: FastifyRequest, reply: FastifyReply) { try { const { fqdn } = request.body + const { DNSServers } = await listSettings(); + if (DNSServers) { + dns.setServers([DNSServers]); + } let ip; try { ip = await dns.resolve(fqdn); diff --git a/apps/api/src/routes/api/v1/settings/types.ts b/apps/api/src/routes/api/v1/settings/types.ts index a33b614a4..d8fcf816d 100644 --- a/apps/api/src/routes/api/v1/settings/types.ts +++ b/apps/api/src/routes/api/v1/settings/types.ts @@ -8,7 +8,8 @@ export interface SaveSettings { minPort: number, maxPort: number, isAutoUpdateEnabled: boolean, - isDNSCheckEnabled: boolean + isDNSCheckEnabled: boolean, + DNSServers: string } } export interface DeleteDomain { diff --git a/apps/api/src/routes/webhooks/traefik/handlers.ts b/apps/api/src/routes/webhooks/traefik/handlers.ts index de805ee71..c4012bc9d 100644 --- a/apps/api/src/routes/webhooks/traefik/handlers.ts +++ b/apps/api/src/routes/webhooks/traefik/handlers.ts @@ -484,7 +484,6 @@ export async function traefikOtherConfiguration(request: FastifyRequest {#if updateStatus.loading} This is useful for storing data such as a database (SQLite) or a cache." + "persistent_storage_explainer": "You can specify any folder that you want to be persistent across deployments.
/example means it will preserve /app/example in the container as /app is the root directory for your application.

This is useful for storing data such as a database (SQLite) or a cache." }, "deployment_queued": "Deployment queued.", "confirm_to_delete": "Are you sure you would like to delete '{{name}}'?", - "stop_application": "Stop application", + "stop_application": "Stop Application", "permission_denied_stop_application": "You do not have permission to stop the application.", - "rebuild_application": "Rebuild application", + "rebuild_application": "Rebuild Application", "permission_denied_rebuild_application": "You do not have permission to rebuild application.", "build_and_start_application": "Deploy", "permission_denied_build_and_start_application": "You do not have permission to deploy application.", diff --git a/apps/ui/src/lib/store.ts b/apps/ui/src/lib/store.ts index 0656ada54..f09872030 100644 --- a/apps/ui/src/lib/store.ts +++ b/apps/ui/src/lib/store.ts @@ -1,3 +1,4 @@ +import { dev } from '$app/env'; import cuid from 'cuid'; import { writable, readable, type Writable } from 'svelte/store'; @@ -71,8 +72,9 @@ export const features = readable({ export const location: Writable = writable(null) export const setLocation = (resource: any, settings?: any) => { - if (resource.settings.isBot) { - return location.set(`http://${settings.ipv4}:${resource.exposePort}`) + if (resource.settings.isBot && resource.exposePort) { + disabledButton.set(false); + return location.set(`http://${dev ? 'localhost' : settings.ipv4}:${resource.exposePort}`) } if (GITPOD_WORKSPACE_URL && resource.exposePort) { const { href } = new URL(GITPOD_WORKSPACE_URL); @@ -84,7 +86,12 @@ export const setLocation = (resource: any, settings?: any) => { const newURL = `https://${CODESANDBOX_HOST.replace(/\$PORT/, resource.exposePort)}` return location.set(newURL) } - return location.set(resource.fqdn) + if (resource.fqdn) { + return location.set(resource.fqdn) + } else { + location.set(null); + disabledButton.set(false); + } } export const toasts: any = writable([]) diff --git a/apps/ui/src/routes/applications/[id]/__layout.svelte b/apps/ui/src/routes/applications/[id]/__layout.svelte index 85f8001d2..c40f66439 100644 --- a/apps/ui/src/routes/applications/[id]/__layout.svelte +++ b/apps/ui/src/routes/applications/[id]/__layout.svelte @@ -77,9 +77,9 @@ const { id } = $page.params; - async function handleDeploySubmit() { + async function handleDeploySubmit(forceRebuild = false) { try { - const { buildId } = await post(`/applications/${id}/deploy`, { ...application }); + const { buildId } = await post(`/applications/${id}/deploy`, { ...application, forceRebuild }); addToast({ message: $t('application.deployment_queued'), type: 'success' @@ -141,8 +141,7 @@ if ( application.gitSourceId && application.destinationDockerId && - application.fqdn && - !application.settings.isBot + (application.fqdn || application.settings.isBot) ) { await getStatus(); statusInterval = setInterval(async () => { @@ -179,9 +178,10 @@ +
+ {/if} -
{#if $status.application.isExited} -
+ handleDeploySubmit(true)}> - - + + + + + + + + + {/if}
+ import { get, post } from '$lib/api'; + import { t } from '$lib/translations'; + import { page } from '$app/stores'; + + import Select from 'svelte-select'; + import Explainer from '$lib/components/Explainer.svelte'; + import { goto } from '$app/navigation'; + import { errorNotification } from '$lib/common'; + + const { id } = $page.params; + + let publicRepositoryLink: string; + let projectId: number; + let repositoryName: string; + let branchName: string; + let ownerName: string; + let type: string; + let branchSelectOptions: any = []; + let loading = { + branches: false + }; + async function loadBranches() { + try { + loading.branches = true; + + const protocol = publicRepositoryLink.split(':')[0]; + const gitUrl = publicRepositoryLink.replace('http://', '').replace('https://', ''); + + let [host, ...path] = gitUrl.split('/'); + const [owner, repository, ...branch] = path; + + ownerName = owner; + repositoryName = repository; + + if (host === 'github.com') { + host = 'api.github.com'; + type = 'github'; + if (branch[0] === 'tree' && branch[1]) { + branchName = branch[1]; + } + } + if (host === 'gitlab.com') { + host = 'gitlab.com/api/v4'; + type = 'gitlab'; + if (branch[1] === 'tree' && branch[2]) { + branchName = branch[2]; + } + } + const apiUrl = `${protocol}://${host}`; + if (type === 'github') { + const repositoryDetails = await get(`${apiUrl}/repos/${ownerName}/${repositoryName}`); + projectId = repositoryDetails.id.toString(); + } + if (type === 'gitlab') { + const repositoryDetails = await get(`${apiUrl}/projects/${ownerName}%2F${repositoryName}`); + projectId = repositoryDetails.id.toString(); + } + if (type === 'github' && branchName) { + try { + await get(`${apiUrl}/repos/${ownerName}/${repositoryName}/branches/${branchName}`); + await saveRepository(); + loading.branches = false; + return; + } catch (error) { + errorNotification(error); + } + } + if (type === 'gitlab' && branchName) { + try { + await get( + `${apiUrl}/projects/${ownerName}%2F${repositoryName}/repository/branches/${branchName}` + ); + await saveRepository(); + loading.branches = false; + return; + } catch (error) { + errorNotification(error); + } + } + let branches: any[] = []; + let page = 1; + let branchCount = 0; + const loadedBranches = await loadBranchesByPage( + apiUrl, + ownerName, + repositoryName, + page, + type + ); + branches = branches.concat(loadedBranches); + branchCount = branches.length; + if (branchCount === 100) { + while (branchCount === 100) { + page = page + 1; + const nextBranches = await loadBranchesByPage( + apiUrl, + ownerName, + repositoryName, + page, + type + ); + branches = branches.concat(nextBranches); + branchCount = nextBranches.length; + } + } + loading.branches = false; + branchSelectOptions = branches.map((branch: any) => ({ + value: branch.name, + label: branch.name + })); + } catch (error) { + return errorNotification(error); + } finally { + loading.branches = false; + } + } + async function loadBranchesByPage( + apiUrl: string, + owner: string, + repository: string, + page = 1, + type: string + ) { + if (type === 'github') { + return await get(`${apiUrl}/repos/${owner}/${repository}/branches?per_page=100&page=${page}`); + } + if (type === 'gitlab') { + return await get( + `${apiUrl}/projects/${ownerName}%2F${repositoryName}/repository/branches?page=${page}` + ); + } + } + async function saveRepository(event?: any) { + try { + if (event?.detail?.value) { + branchName = event.detail.value; + } + await post(`/applications/${id}/configuration/source`, { + gitSourceId: null, + forPublic: true, + type + }); + await post(`/applications/${id}/configuration/repository`, { + repository: `${ownerName}/${repositoryName}`, + branch: branchName, + projectId, + autodeploy: false, + webhookToken: null, + isPublicRepository: true + }); + + return await goto(`/applications/${id}/configuration/destination`); + } catch (error) { + return errorNotification(error); + } + } + + +
+
+
+ +
+ + {#if branchSelectOptions.length > 0} +
+ + {#if isDisabled || application.settings.isPublicRepository} + {:else} {$t('application.git_repository')} - {#if isDisabled} - + {#if isDisabled || application.settings.isPublicRepository} + {:else} changeSettings('isBot')} title="Is your application a bot?" - description="You can deploy applications without domains.
They will listen on IP:PORT instead.

Useful for example bots." + description="You can deploy applications without domains.
You can also make them to listen on IP:EXPOSEDPORT as well.

Useful to host Twitch bots, regular jobs, or anything that does not require an incoming connection." + disabled={$status.application.isRunning} />
{#if !isBot} @@ -611,7 +620,7 @@
{/if} {/if} - {#if !staticDeployments.includes(application.buildPack) && !isBot} + {#if !staticDeployments.includes(application.buildPack)}
+
{/if}
@@ -632,7 +644,6 @@ name="exposePort" id="exposePort" bind:value={application.exposePort} - required={isBot} placeholder="12345" /> {$t('application.features')}
-
- changeSettings('autodeploy')} - title={$t('application.enable_automatic_deployment')} - description={$t('application.enable_auto_deploy_webhooks')} - /> -
-
- changeSettings('previews')} - title={$t('application.enable_mr_pr_previews')} - description={$t('application.enable_preview_deploy_mr_pr_requests')} - /> -
+ {#if !application.settings.isPublicRepository} +
+ changeSettings('autodeploy')} + title={$t('application.enable_automatic_deployment')} + description={$t('application.enable_auto_deploy_webhooks')} + /> +
+ {/if} + {#if !application.settings.isBot} +
+ changeSettings('previews')} + title={$t('application.enable_mr_pr_previews')} + description={$t('application.enable_preview_deploy_mr_pr_requests')} + /> +
+ {/if}
-
- -
+ @@ -109,4 +107,7 @@
+
+ +
diff --git a/apps/ui/src/routes/applications/index.svelte b/apps/ui/src/routes/applications/index.svelte index 8c7b8fe73..6dbb09ee4 100644 --- a/apps/ui/src/routes/applications/index.svelte +++ b/apps/ui/src/routes/applications/index.svelte @@ -87,6 +87,9 @@ {#if application.fqdn}
{getDomain(application.fqdn) || ''}
{/if} + {#if application.settings.isBot} +
BOT
+ {/if} {#if application.destinationDocker?.name}
{application.destinationDocker.name}
{/if} @@ -98,7 +101,7 @@
Destination Missing
- {:else if !application.fqdn} + {:else if !application.fqdn && !application.settings.isBot}
URL Missing
diff --git a/apps/ui/src/routes/index.svelte b/apps/ui/src/routes/index.svelte index f28009843..63ae5fa47 100644 --- a/apps/ui/src/routes/index.svelte +++ b/apps/ui/src/routes/index.svelte @@ -20,35 +20,38 @@ -
+
Ghost
+
diff --git a/apps/ui/src/routes/settings/global.svelte b/apps/ui/src/routes/settings/global.svelte index 42bfc633f..1c09d156e 100644 --- a/apps/ui/src/routes/settings/global.svelte +++ b/apps/ui/src/routes/settings/global.svelte @@ -31,7 +31,7 @@ let dualCerts = settings.dualCerts; let isAutoUpdateEnabled = settings.isAutoUpdateEnabled; let isDNSCheckEnabled = settings.isDNSCheckEnabled; - + let DNSServers = settings.DNSServers; let minPort = settings.minPort; let maxPort = settings.maxPort; @@ -105,6 +105,10 @@ settings.minPort = minPort; settings.maxPort = maxPort; } + if (DNSServers !== settings.DNSServers) { + await post(`/settings`, { DNSServers }); + settings.DNSServers = DNSServers; + } forceSave = false; return addToast({ message: 'Configuration saved.', @@ -275,6 +279,17 @@ on:click={() => changeSettings('isDNSCheckEnabled')} />
+
+
+
+ Custom DNS servers +
+ +
+
+ +
+