Merge pull request #546 from coollabsio/public_repos

Import public repositories feature and more
This commit is contained in:
Andras Bacsai 2022-08-18 20:56:08 +02:00 committed by GitHub
commit 70717dcbe5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 606 additions and 278 deletions

View File

@ -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;

View File

@ -126,6 +126,7 @@ model ApplicationSettings {
previews Boolean @default(false) previews Boolean @default(false)
autodeploy Boolean @default(true) autodeploy Boolean @default(true)
isBot Boolean @default(false) isBot Boolean @default(false)
isPublicRepository Boolean @default(false)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
application Application @relation(fields: [applicationId], references: [id]) application Application @relation(fields: [applicationId], references: [id])
@ -238,6 +239,7 @@ model SshKey {
model GitSource { model GitSource {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
forPublic Boolean @default(false)
type String? type String?
apiUrl String? apiUrl String?
htmlUrl String? htmlUrl String?

View File

@ -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() main()
.catch((e) => { .catch((e) => {

View File

@ -56,6 +56,7 @@ import * as buildpacks from '../lib/buildPacks';
baseImage, baseImage,
baseBuildImage, baseBuildImage,
deploymentType, deploymentType,
forceRebuild
} = message } = message
let { let {
branch, branch,
@ -69,6 +70,30 @@ import * as buildpacks from '../lib/buildPacks';
dockerFileLocation, dockerFileLocation,
denoMainFile denoMainFile
} = message } = 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 { try {
const { debug } = settings; const { debug } = settings;
if (concurrency === 1) { if (concurrency === 1) {
@ -131,7 +156,8 @@ import * as buildpacks from '../lib/buildPacks';
htmlUrl: gitSource.htmlUrl, htmlUrl: gitSource.htmlUrl,
projectId, projectId,
deployKeyId: gitSource.gitlabApp?.deployKeyId || null, deployKeyId: gitSource.gitlabApp?.deployKeyId || null,
privateSshKey: decrypt(gitSource.gitlabApp?.privateSshKey) || null privateSshKey: decrypt(gitSource.gitlabApp?.privateSshKey) || null,
forPublic: gitSource.forPublic
}); });
if (!commit) { if (!commit) {
throw new Error('No commit found?'); throw new Error('No commit found?');
@ -146,38 +172,10 @@ import * as buildpacks from '../lib/buildPacks';
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
if (!pullmergeRequestId) { 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) { if (configHash !== currentHash) {
await prisma.application.update({
where: { id: applicationId },
data: { configHash: currentHash }
});
deployNeeded = true; deployNeeded = true;
if (configHash) { if (configHash) {
await saveBuildLog({ line: 'Configuration changed.', buildId, applicationId }); await saveBuildLog({ line: 'Configuration changed.', buildId, applicationId });
@ -200,6 +198,8 @@ import * as buildpacks from '../lib/buildPacks';
// //
} }
await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId, baseImage); await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId, baseImage);
if (forceRebuild) deployNeeded = true
if (!imageFound || deployNeeded) { if (!imageFound || deployNeeded) {
// if (true) { // if (true) {
if (buildpacks[buildPack]) if (buildpacks[buildPack])
@ -250,7 +250,9 @@ import * as buildpacks from '../lib/buildPacks';
} catch (error) { } catch (error) {
// //
} }
const envs = []; const envs = [
`PORT=${port}`
];
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (pullmergeRequestId) { if (pullmergeRequestId) {
@ -336,6 +338,10 @@ import * as buildpacks from '../lib/buildPacks';
} }
await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId }); await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId });
await prisma.build.update({ where: { id: message.build_id }, data: { status: 'success' } }); await prisma.build.update({ where: { id: message.build_id }, data: { status: 'success' } });
if (!pullmergeRequestId) await prisma.application.update({
where: { id: applicationId },
data: { configHash: currentHash }
});
} }
} }

View File

@ -17,7 +17,7 @@ import { checkContainer, removeContainer } from './docker';
import { day } from './dayjs'; import { day } from './dayjs';
import * as serviceFields from './serviceFields' import * as serviceFields from './serviceFields'
export const version = '3.5.2'; export const version = '3.6.0';
export const isDev = process.env.NODE_ENV === 'development'; export const isDev = process.env.NODE_ENV === 'development';
const algorithm = 'aes-256-ctr'; const algorithm = 'aes-256-ctr';
@ -1176,6 +1176,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) { export async function getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress) {
const { default: getPort } = await import('get-port'); const { default: getPort } = await import('get-port');
const applicationUsed = await ( const applicationUsed = await (

View File

@ -71,7 +71,6 @@ export async function removeContainer({
}): Promise<void> { }): Promise<void> {
try { try {
const { stdout } = await executeDockerCmd({ dockerId, command: `docker inspect --format '{{json .State}}' ${id}` }) const { stdout } = await executeDockerCmd({ dockerId, command: `docker inspect --format '{{json .State}}' ${id}` })
console.log(id)
if (JSON.parse(stdout).Running) { if (JSON.parse(stdout).Running) {
await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` }) await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` })
await executeDockerCmd({ dockerId, command: `docker rm ${id}` }) await executeDockerCmd({ dockerId, command: `docker rm ${id}` })

View File

@ -12,7 +12,8 @@ export default async function ({
htmlUrl, htmlUrl,
branch, branch,
buildId, buildId,
customPort customPort,
forPublic
}: { }: {
applicationId: string; applicationId: string;
workdir: string; workdir: string;
@ -23,15 +24,25 @@ export default async function ({
branch: string; branch: string;
buildId: string; buildId: string;
customPort: number; customPort: number;
forPublic?: boolean;
}): Promise<string> { }): Promise<string> {
const { default: got } = await import('got') const { default: got } = await import('got')
const url = htmlUrl.replace('https://', '').replace('http://', ''); const url = htmlUrl.replace('https://', '').replace('http://', '');
await saveBuildLog({ line: 'GitHub importer started.', buildId, applicationId }); 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 .. `
);
} else {
const body = await prisma.githubApp.findUnique({ where: { id: githubAppId } }); const body = await prisma.githubApp.findUnique({ where: { id: githubAppId } });
if (body.privateKey) body.privateKey = decrypt(body.privateKey); if (body.privateKey) body.privateKey = decrypt(body.privateKey);
const { privateKey, appId, installationId } = body 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 = { const payload = {
@ -58,6 +69,10 @@ export default async function ({
await asyncExecShell( 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 .. ` `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`); const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`);
return commit.replace('\n', ''); return commit.replace('\n', '');
} }

View File

@ -5,7 +5,7 @@ import axios from 'axios';
import { FastifyReply } from 'fastify'; import { FastifyReply } from 'fastify';
import { day } from '../../../../lib/dayjs'; import { day } from '../../../../lib/dayjs';
import { setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common'; 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 { checkContainer, formatLabelsOnDocker, isContainerExited, removeContainer } from '../../../../lib/docker';
import { scheduler } from '../../../../lib/scheduler'; import { scheduler } from '../../../../lib/scheduler';
@ -238,6 +238,9 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
if (exposePort) { if (exposePort) {
exposePort = Number(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(); if (denoOptions) denoOptions = denoOptions.trim();
const defaultConfiguration = await setDefaultConfiguration({ const defaultConfiguration = await setDefaultConfiguration({
buildPack, buildPack,
@ -392,18 +395,7 @@ export async function checkDNS(request: FastifyRequest<CheckDNS>) {
if (found) { if (found) {
throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` } throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` }
} }
if (exposePort) { await checkExposedPort({ id, configuredPort, exposePort, dockerId, remoteIpAddress })
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.` }
}
}
}
if (isDNSCheckEnabled && !isDev && !forceSave) { if (isDNSCheckEnabled && !isDev && !forceSave) {
let hostname = request.hostname.split(':')[0]; let hostname = request.hostname.split(':')[0];
if (remoteEngine) hostname = remoteIpAddress; if (remoteEngine) hostname = remoteIpAddress;
@ -436,7 +428,7 @@ export async function deployApplication(request: FastifyRequest<DeployApplicatio
try { try {
const { id } = request.params const { id } = request.params
const teamId = request.user?.teamId; const teamId = request.user?.teamId;
const { pullmergeRequestId = null, branch } = request.body const { pullmergeRequestId = null, branch, forceRebuild } = request.body
const buildId = cuid(); const buildId = cuid();
const application = await getApplicationFromDB(id, teamId); const application = await getApplicationFromDB(id, teamId);
if (application) { if (application) {
@ -475,13 +467,15 @@ export async function deployApplication(request: FastifyRequest<DeployApplicatio
type: 'manual', type: 'manual',
...application, ...application,
sourceBranch: branch, sourceBranch: branch,
pullmergeRequestId pullmergeRequestId,
forceRebuild
}); });
} else { } else {
scheduler.workers.get('deployApplication').postMessage({ scheduler.workers.get('deployApplication').postMessage({
build_id: buildId, build_id: buildId,
type: 'manual', type: 'manual',
...application ...application,
forceRebuild
}); });
} }
@ -499,11 +493,20 @@ export async function deployApplication(request: FastifyRequest<DeployApplicatio
export async function saveApplicationSource(request: FastifyRequest<SaveApplicationSource>, reply: FastifyReply) { export async function saveApplicationSource(request: FastifyRequest<SaveApplicationSource>, reply: FastifyReply) {
try { try {
const { id } = request.params const { id } = request.params
const { gitSourceId } = request.body 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({ await prisma.application.update({
where: { id }, where: { id },
data: { gitSource: { connect: { id: gitSourceId } } } data: { gitSource: { connect: { id: gitSourceId } } }
}); });
}
return reply.code(201).send() return reply.code(201).send()
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
@ -557,7 +560,7 @@ export async function checkRepository(request: FastifyRequest<CheckRepository>)
export async function saveRepository(request, reply) { export async function saveRepository(request, reply) {
try { try {
const { id } = request.params 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(); repository = repository.toLowerCase();
branch = branch.toLowerCase(); branch = branch.toLowerCase();
@ -565,17 +568,19 @@ export async function saveRepository(request, reply) {
if (webhookToken) { if (webhookToken) {
await prisma.application.update({ await prisma.application.update({
where: { id }, 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 { } else {
await prisma.application.update({ await prisma.application.update({
where: { id }, where: { id },
data: { repository, branch, projectId, settings: { update: { autodeploy } } } data: { repository, branch, projectId, settings: { update: { autodeploy, isPublicRepository } } }
}); });
} }
if (!isPublicRepository) {
const isDouble = await checkDoubleBranch(branch, projectId); const isDouble = await checkDoubleBranch(branch, projectId);
if (isDouble) { if (isDouble) {
await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false } }) await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false, isPublicRepository } })
}
} }
return reply.code(201).send() return reply.code(201).send()
} catch ({ status, message }) { } catch ({ status, message }) {
@ -607,7 +612,8 @@ export async function getBuildPack(request) {
projectId: application.projectId, projectId: application.projectId,
repository: application.repository, repository: application.repository,
branch: application.branch, branch: application.branch,
apiUrl: application.gitSource.apiUrl apiUrl: application.gitSource.apiUrl,
isPublicRepository: application.settings.isPublicRepository
} }
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
@ -658,7 +664,6 @@ export async function saveSecret(request: FastifyRequest<SaveSecret>, reply: Fas
throw { status: 500, message: `Secret ${name} already exists.` } throw { status: 500, message: `Secret ${name} already exists.` }
} else { } else {
value = encrypt(value.trim()); value = encrypt(value.trim());
console.log({value})
await prisma.secret.create({ await prisma.secret.create({
data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } } data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } }
}); });

View File

@ -44,13 +44,13 @@ export interface CheckDNS extends OnlyId {
} }
export interface DeployApplication { export interface DeployApplication {
Querystring: { domain: string } Querystring: { domain: string }
Body: { pullmergeRequestId: string | null, branch: string } Body: { pullmergeRequestId: string | null, branch: string, forceRebuild?: boolean }
} }
export interface GetImages { export interface GetImages {
Body: { buildPack: string, deploymentType: string } Body: { buildPack: string, deploymentType: string }
} }
export interface SaveApplicationSource extends OnlyId { export interface SaveApplicationSource extends OnlyId {
Body: { gitSourceId: string } Body: { gitSourceId?: string | null, forPublic?: boolean, type?: string }
} }
export interface CheckRepository extends OnlyId { export interface CheckRepository extends OnlyId {
Querystring: { repository: string, branch: string } Querystring: { repository: string, branch: string }
@ -115,7 +115,8 @@ export interface CancelDeployment {
export interface DeployApplication extends OnlyId { export interface DeployApplication extends OnlyId {
Body: { Body: {
pullmergeRequestId: string | null, pullmergeRequestId: string | null,
branch: string branch: string,
forceRebuild?: boolean
} }
} }

View File

@ -79,7 +79,6 @@ export async function newDestination(request: FastifyRequest<NewDestination>, re
let { name, network, engine, isCoolifyProxyUsed, remoteIpAddress, remoteUser, remotePort } = request.body let { name, network, engine, isCoolifyProxyUsed, remoteIpAddress, remoteUser, remotePort } = request.body
if (id === 'new') { if (id === 'new') {
console.log(engine)
if (engine) { if (engine) {
const { stdout } = await asyncExecShell(`DOCKER_HOST=unix:///var/run/docker.sock docker network ls --filter 'name=^${network}$' --format '{{json .}}'`); const { stdout } = await asyncExecShell(`DOCKER_HOST=unix:///var/run/docker.sock docker network ls --filter 'name=^${network}$' --format '{{json .}}'`);
if (stdout === '') { if (stdout === '') {

View File

@ -2,7 +2,7 @@ import type { FastifyReply, FastifyRequest } from 'fastify';
import fs from 'fs/promises'; import fs from 'fs/promises';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import bcrypt from 'bcryptjs'; 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, defaultComposeConfiguration } 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 { day } from '../../../../lib/dayjs';
import { checkContainer, isContainerExited, removeContainer } from '../../../../lib/docker'; import { checkContainer, isContainerExited, removeContainer } from '../../../../lib/docker';
import cuid from 'cuid'; import cuid from 'cuid';
@ -378,18 +378,7 @@ export async function checkService(request: FastifyRequest<CheckService>) {
} }
} }
} }
if (exposePort) { await checkExposedPort({ id, configuredPort, exposePort, dockerId, remoteIpAddress })
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.` }
}
}
}
if (isDNSCheckEnabled && !isDev && !forceSave) { if (isDNSCheckEnabled && !isDev && !forceSave) {
let hostname = request.hostname.split(':')[0]; let hostname = request.hostname.split(':')[0];
if (remoteEngine) hostname = remoteIpAddress; if (remoteEngine) hostname = remoteIpAddress;

View File

@ -484,7 +484,6 @@ export async function traefikOtherConfiguration(request: FastifyRequest<TraefikO
} }
throw { status: 500 } throw { status: 500 }
} catch ({ status, message }) { } catch ({ status, message }) {
console.log(status, message);
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }

View File

@ -159,7 +159,7 @@
"storage_saved": "Storage saved.", "storage_saved": "Storage saved.",
"storage_updated": "Storage updated.", "storage_updated": "Storage updated.",
"storage_deleted": "Storage deleted.", "storage_deleted": "Storage deleted.",
"persistent_storage_explainer": "You can specify any folder that you want to be persistent across deployments. <br>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.<br><span class='text-green-500 font-bold'>/example</span> means it will preserve <span class='text-green-500 font-bold'>/app/example</span> in the container as <span class='text-green-500 font-bold'>/app</span> is <span class='text-green-500 font-bold'>the root directory</span> for your application.<br><br>This is useful for storing data such as a <span class='text-green-500 font-bold'>database (SQLite)</span> or a <span class='text-green-500 font-bold'>cache</span>."
}, },
"deployment_queued": "Deployment queued.", "deployment_queued": "Deployment queued.",
"confirm_to_delete": "Are you sure you would like to delete '{{name}}'?", "confirm_to_delete": "Are you sure you would like to delete '{{name}}'?",

View File

@ -77,9 +77,9 @@
const { id } = $page.params; const { id } = $page.params;
async function handleDeploySubmit() { async function handleDeploySubmit(forceRebuild = false) {
try { try {
const { buildId } = await post(`/applications/${id}/deploy`, { ...application }); const { buildId } = await post(`/applications/${id}/deploy`, { ...application, forceRebuild });
addToast({ addToast({
message: $t('application.deployment_queued'), message: $t('application.deployment_queued'),
type: 'success' type: 'success'
@ -141,8 +141,7 @@
if ( if (
application.gitSourceId && application.gitSourceId &&
application.destinationDockerId && application.destinationDockerId &&
(application.fqdn || (application.fqdn || application.settings.isBot)
application.settings.isBot)
) { ) {
await getStatus(); await getStatus();
statusInterval = setInterval(async () => { statusInterval = setInterval(async () => {
@ -256,7 +255,7 @@
<rect x="14" y="5" width="4" height="14" rx="1" /> <rect x="14" y="5" width="4" height="14" rx="1" />
</svg> </svg>
</button> </button>
<form on:submit|preventDefault={handleDeploySubmit}> <form on:submit|preventDefault={() => handleDeploySubmit(true)}>
<button <button
type="submit" type="submit"
disabled={$disabledButton || !isQueueActive} disabled={$disabledButton || !isQueueActive}
@ -264,7 +263,7 @@
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm flex items-center space-x-2" class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm flex items-center space-x-2"
data-tip={$appSession.isAdmin data-tip={$appSession.isAdmin
? isQueueActive ? isQueueActive
? 'Rebuild Application' ? 'Force Rebuild Application'
: 'Autoupdate inprogress. Cannot rebuild application.' : 'Autoupdate inprogress. Cannot rebuild application.'
: 'You do not have permission to rebuild application.'} : 'You do not have permission to rebuild application.'}
> >

View File

@ -26,7 +26,7 @@
delete tempBuildPack.color; delete tempBuildPack.color;
delete tempBuildPack.hoverColor; delete tempBuildPack.hoverColor;
if (foundConfig.buildPack !== name) { if (foundConfig?.buildPack !== name) {
await post(`/applications/${id}`, { ...tempBuildPack, buildPack: name }); await post(`/applications/${id}`, { ...tempBuildPack, buildPack: name });
} }
await post(`/applications/${id}/configuration/buildpack`, { buildPack: name }); await post(`/applications/${id}/configuration/buildpack`, { buildPack: name });

View File

@ -0,0 +1,199 @@
<script lang="ts">
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);
}
}
</script>
<div class="mx-auto max-w-5xl">
<div class="grid grid-flow-row gap-2 px-10">
<div class="flex">
<form class="flex" on:submit|preventDefault={loadBranches}>
<div class="space-y-4">
<input
placeholder="eg: https://github.com/coollabsio/nodejs-example/tree/main"
class="text-xs"
bind:value={publicRepositoryLink}
/>
{#if branchSelectOptions.length > 0}
<div class="custom-select-wrapper">
<Select
placeholder={loading.branches
? $t('application.configuration.loading_branches')
: !publicRepositoryLink
? $t('application.configuration.select_a_repository_first')
: $t('application.configuration.select_a_branch')}
isWaiting={loading.branches}
showIndicator={!!publicRepositoryLink && !loading.branches}
id="branches"
on:select={saveRepository}
items={branchSelectOptions}
isDisabled={loading.branches || !!!publicRepositoryLink}
isClearable={false}
/>
</div>
{/if}
</div>
<button class="btn mx-4 bg-orange-600" class:loading={loading.branches} type="submit"
>Load Repository</button
>
</form>
</div>
</div>
<Explainer
text="Examples:<br><br>https://github.com/coollabsio/nodejs-example<br>https://github.com/coollabsio/nodejs-example/tree/main<br>https://gitlab.com/aleveha/fastify-example<br>https://gitlab.com/aleveha/fastify-example/-/tree/master<br><br>Only works with Github.com and Gitlab.com."
/>
</div>

View File

@ -47,6 +47,7 @@
export let branch: any; export let branch: any;
export let type: any; export let type: any;
export let application: any; export let application: any;
export let isPublicRepository: boolean;
function checkPackageJSONContents({ key, json }: { key: any; json: any }) { function checkPackageJSONContents({ key, json }: { key: any; json: any }) {
return json?.dependencies?.hasOwnProperty(key) || json?.devDependencies?.hasOwnProperty(key); return json?.dependencies?.hasOwnProperty(key) || json?.devDependencies?.hasOwnProperty(key);
@ -236,7 +237,7 @@
if (error.message === 'Bad credentials') { if (error.message === 'Bad credentials') {
const { token } = await get(`/applications/${id}/configuration/githubToken`); const { token } = await get(`/applications/${id}/configuration/githubToken`);
$appSession.tokens.github = token; $appSession.tokens.github = token;
return await scanRepository() return await scanRepository();
} }
return errorNotification(error); return errorNotification(error);
} finally { } finally {
@ -245,7 +246,11 @@
} }
} }
onMount(async () => { onMount(async () => {
if (!isPublicRepository) {
await scanRepository(); await scanRepository();
} else {
scanning = false;
}
}); });
</script> </script>
@ -262,12 +267,10 @@
</div> </div>
</div> </div>
{:else} {:else}
<div class="max-w-5xl mx-auto ">
<div class="title pb-2">Coolify</div>
<div class="max-w-7xl mx-auto ">
<div class="title pb-2">Coolify Buildpacks</div>
<div class="flex flex-wrap justify-center"> <div class="flex flex-wrap justify-center">
{#each buildPacks.filter(bp => bp.isCoolifyBuildPack === true) as buildPack} {#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true) as buildPack}
<div class="p-2"> <div class="p-2">
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig /> <BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
</div> </div>
@ -275,10 +278,10 @@
</div> </div>
</div> </div>
<div class="max-w-7xl mx-auto "> <div class="max-w-5xl mx-auto ">
<div class="title pb-2">Heroku</div> <div class="title pb-2">Other</div>
<div class="flex flex-wrap justify-center"> <div class="flex flex-wrap justify-center">
{#each buildPacks.filter(bp => bp.isHerokuBuildPack === true) as buildPack} {#each buildPacks.filter((bp) => bp.isHerokuBuildPack === true) as buildPack}
<div class="p-2"> <div class="p-2">
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig /> <BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
</div> </div>

View File

@ -48,3 +48,4 @@
<GitlabRepositories {application} {appId} {settings} /> <GitlabRepositories {application} {appId} {settings} />
{/if} {/if}
</div> </div>

View File

@ -31,6 +31,8 @@
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import { appSession } from '$lib/store'; import { appSession } from '$lib/store';
import PublicRepository from './_PublicRepository.svelte';
import Explainer from '$lib/components/Explainer.svelte';
const { id } = $page.params; const { id } = $page.params;
const from = $page.url.searchParams.get('from'); const from = $page.url.searchParams.get('from');
@ -71,7 +73,9 @@
{$t('application.configuration.select_a_git_source')} {$t('application.configuration.select_a_git_source')}
</div> </div>
</div> </div>
<div class="flex flex-col justify-center"> <div class="max-w-5xl mx-auto ">
<div class="title pb-8">Git App</div>
<div class="flex flex-wrap justify-center">
{#if !filteredSources || ownSources.length === 0} {#if !filteredSources || ownSources.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="pb-2 text-center font-bold"> <div class="pb-2 text-center font-bold">
@ -187,4 +191,8 @@
{/each} {/each}
</div> </div>
{/if} {/if}
</div>
<div class="title py-4">Public Repository</div>
<PublicRepository />
</div> </div>

View File

@ -133,6 +133,7 @@
autodeploy = !autodeploy; autodeploy = !autodeploy;
} }
if (name === 'isBot') { if (name === 'isBot') {
if ($status.application.isRunning) return;
isBot = !isBot; isBot = !isBot;
application.settings.isBot = isBot; application.settings.isBot = isBot;
setLocation(application, settings); setLocation(application, settings);
@ -345,8 +346,11 @@
<label for="gitSource" class="text-base font-bold text-stone-100" <label for="gitSource" class="text-base font-bold text-stone-100"
>{$t('application.git_source')}</label >{$t('application.git_source')}</label
> >
{#if isDisabled} {#if isDisabled || application.settings.isPublicRepository}
<input disabled={isDisabled} value={application.gitSource.name} /> <input
disabled={isDisabled || application.settings.isPublicRepository}
value={application.gitSource.name}
/>
{:else} {:else}
<a <a
href={`/applications/${id}/configuration/source?from=/applications/${id}`} href={`/applications/${id}/configuration/source?from=/applications/${id}`}
@ -363,8 +367,11 @@
<label for="repository" class="text-base font-bold text-stone-100" <label for="repository" class="text-base font-bold text-stone-100"
>{$t('application.git_repository')}</label >{$t('application.git_repository')}</label
> >
{#if isDisabled} {#if isDisabled || application.settings.isPublicRepository}
<input disabled={isDisabled} value="{application.repository}/{application.branch}" /> <input
disabled={isDisabled || application.settings.isPublicRepository}
value="{application.repository}/{application.branch}"
/>
{:else} {:else}
<a <a
href={`/applications/${id}/configuration/repository?from=/applications/${id}&to=/applications/${id}/configuration/buildpack`} href={`/applications/${id}/configuration/repository?from=/applications/${id}&to=/applications/${id}/configuration/buildpack`}
@ -488,6 +495,7 @@
on:click={() => changeSettings('isBot')} on:click={() => changeSettings('isBot')}
title="Is your application a bot?" title="Is your application a bot?"
description="You can deploy applications without domains. <br>They will listen on <span class='text-green-500 font-bold'>IP:EXPOSEDPORT</span> instead.<br></Setting><br>Useful to host <span class='text-green-500 font-bold'>Twitch bots.</span>" description="You can deploy applications without domains. <br>They will listen on <span class='text-green-500 font-bold'>IP:EXPOSEDPORT</span> instead.<br></Setting><br>Useful to host <span class='text-green-500 font-bold'>Twitch bots.</span>"
disabled={$status.application.isRunning}
/> />
</div> </div>
{#if !isBot} {#if !isBot}
@ -612,7 +620,7 @@
</div> </div>
{/if} {/if}
{/if} {/if}
{#if !staticDeployments.includes(application.buildPack) && !isBot} {#if !staticDeployments.includes(application.buildPack)}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="port" class="text-base font-bold text-stone-100">{$t('forms.port')}</label> <label for="port" class="text-base font-bold text-stone-100">{$t('forms.port')}</label>
<input <input
@ -623,6 +631,9 @@
bind:value={application.port} bind:value={application.port}
placeholder="{$t('forms.default')}: 'python' ? '8000' : '3000'" placeholder="{$t('forms.default')}: 'python' ? '8000' : '3000'"
/> />
<Explainer
text={'The port your application listens on.'}
/>
</div> </div>
{/if} {/if}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
@ -770,6 +781,7 @@
<div class="title">{$t('application.features')}</div> <div class="title">{$t('application.features')}</div>
</div> </div>
<div class="px-10 pb-10"> <div class="px-10 pb-10">
{#if !application.settings.isPublicRepository}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
isCenter={false} isCenter={false}
@ -779,6 +791,7 @@
description={$t('application.enable_auto_deploy_webhooks')} description={$t('application.enable_auto_deploy_webhooks')}
/> />
</div> </div>
{/if}
{#if !application.settings.isBot} {#if !application.settings.isBot}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<Setting <Setting

View File

@ -109,7 +109,7 @@
<div class="flex justify-end sticky top-0 p-2 mx-1"> <div class="flex justify-end sticky top-0 p-2 mx-1">
<button <button
on:click={followBuild} on:click={followBuild}
class="bg-transparent btn btn-sm tooltip tooltip-primary tooltip-bottom hover:text-green-500 hover:bg-coolgray-500" class="bg-transparent btn btn-sm btn-link tooltip tooltip-primary tooltip-bottom hover:text-green-500 hover:bg-coolgray-500"
data-tip="Follow logs" data-tip="Follow logs"
class:text-green-500={followingBuild} class:text-green-500={followingBuild}
> >

View File

@ -147,7 +147,7 @@
<div class="flex justify-end sticky top-0 p-1 mx-1"> <div class="flex justify-end sticky top-0 p-1 mx-1">
<button <button
on:click={followBuild} on:click={followBuild}
class="bg-transparent btn btn-sm tooltip tooltip-primary tooltip-bottom" class="bg-transparent btn btn-sm btn-link tooltip tooltip-primary tooltip-bottom"
data-tip="Follow logs" data-tip="Follow logs"
class:text-green-500={followingLogs} class:text-green-500={followingLogs}
> >

View File

@ -87,9 +87,7 @@
</div> </div>
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4"> <div class="mx-auto max-w-6xl rounded-xl px-6 pt-4">
<div class="flex justify-center py-4 text-center">
<Explainer customClass="w-full" text={$t('application.storage.persistent_storage_explainer')} />
</div>
<table class="mx-auto border-separate text-left"> <table class="mx-auto border-separate text-left">
<thead> <thead>
<tr class="h-12"> <tr class="h-12">
@ -109,4 +107,7 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="flex justify-center py-4 text-center">
<Explainer customClass="w-full" text={$t('application.storage.persistent_storage_explainer')} />
</div>
</div> </div>

View File

@ -1,7 +1,7 @@
{ {
"name": "coolify", "name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.", "description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "3.5.2", "version": "3.6.0",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": "github:coollabsio/coolify", "repository": "github:coollabsio/coolify",
"scripts": { "scripts": {