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)
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])
@ -238,6 +239,7 @@ model SshKey {
model GitSource {
id String @id @default(cuid())
name String
forPublic Boolean @default(false)
type String?
apiUrl 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()
.catch((e) => {

View File

@ -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,6 +198,8 @@ import * as buildpacks from '../lib/buildPacks';
//
}
await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId, baseImage);
if (forceRebuild) deployNeeded = true
if (!imageFound || deployNeeded) {
// if (true) {
if (buildpacks[buildPack])
@ -250,7 +250,9 @@ import * as buildpacks from '../lib/buildPacks';
} catch (error) {
//
}
const envs = [];
const envs = [
`PORT=${port}`
];
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (pullmergeRequestId) {
@ -336,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 }
});
}
}

View File

@ -17,7 +17,7 @@ import { checkContainer, removeContainer } from './docker';
import { day } from './dayjs';
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';
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) {
const { default: getPort } = await import('get-port');
const applicationUsed = await (

View File

@ -71,7 +71,6 @@ export async function removeContainer({
}): Promise<void> {
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}` })

View File

@ -12,7 +12,8 @@ export default async function ({
htmlUrl,
branch,
buildId,
customPort
customPort,
forPublic
}: {
applicationId: string;
workdir: string;
@ -23,15 +24,25 @@ export default async function ({
branch: string;
buildId: string;
customPort: number;
forPublic?: boolean;
}): Promise<string> {
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 .. `
);
} 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 payload = {
@ -58,6 +69,10 @@ export default async function ({
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', '');
}
const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`);
return commit.replace('\n', '');
}

View File

@ -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';
@ -238,6 +238,9 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
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<CheckDNS>) {
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<DeployApplicatio
try {
const { id } = request.params
const teamId = request.user?.teamId;
const { pullmergeRequestId = null, branch } = request.body
const { pullmergeRequestId = null, branch, forceRebuild } = request.body
const buildId = cuid();
const application = await getApplicationFromDB(id, teamId);
if (application) {
@ -475,13 +467,15 @@ export async function deployApplication(request: FastifyRequest<DeployApplicatio
type: 'manual',
...application,
sourceBranch: branch,
pullmergeRequestId
pullmergeRequestId,
forceRebuild
});
} else {
scheduler.workers.get('deployApplication').postMessage({
build_id: buildId,
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) {
try {
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({
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<CheckRepository>)
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 } } }
});
}
if (!isPublicRepository) {
const isDouble = await checkDoubleBranch(branch, projectId);
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()
} 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 })
@ -658,7 +664,6 @@ export async function saveSecret(request: FastifyRequest<SaveSecret>, reply: Fas
throw { status: 500, message: `Secret ${name} already exists.` }
} else {
value = encrypt(value.trim());
console.log({value})
await prisma.secret.create({
data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } }
});

View File

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

View File

@ -79,7 +79,6 @@ export async function newDestination(request: FastifyRequest<NewDestination>, 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 === '') {

View File

@ -2,7 +2,7 @@ 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, 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 { checkContainer, isContainerExited, removeContainer } from '../../../../lib/docker';
import cuid from 'cuid';
@ -378,18 +378,7 @@ export async function checkService(request: FastifyRequest<CheckService>) {
}
}
}
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;

View File

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

View File

@ -159,7 +159,7 @@
"storage_saved": "Storage saved.",
"storage_updated": "Storage updated.",
"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.",
"confirm_to_delete": "Are you sure you would like to delete '{{name}}'?",

View File

@ -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 () => {
@ -256,7 +255,7 @@
<rect x="14" y="5" width="4" height="14" rx="1" />
</svg>
</button>
<form on:submit|preventDefault={handleDeploySubmit}>
<form on:submit|preventDefault={() => handleDeploySubmit(true)}>
<button
type="submit"
disabled={$disabledButton || !isQueueActive}
@ -264,7 +263,7 @@
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm flex items-center space-x-2"
data-tip={$appSession.isAdmin
? isQueueActive
? 'Rebuild Application'
? 'Force Rebuild Application'
: 'Autoupdate inprogress. Cannot rebuild application.'
: 'You do not have permission to rebuild application.'}
>

View File

@ -26,7 +26,7 @@
delete tempBuildPack.color;
delete tempBuildPack.hoverColor;
if (foundConfig.buildPack !== name) {
if (foundConfig?.buildPack !== name) {
await post(`/applications/${id}`, { ...tempBuildPack, 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 type: any;
export let application: any;
export let isPublicRepository: boolean;
function checkPackageJSONContents({ key, json }: { key: any; json: any }) {
return json?.dependencies?.hasOwnProperty(key) || json?.devDependencies?.hasOwnProperty(key);
@ -236,7 +237,7 @@
if (error.message === 'Bad credentials') {
const { token } = await get(`/applications/${id}/configuration/githubToken`);
$appSession.tokens.github = token;
return await scanRepository()
return await scanRepository();
}
return errorNotification(error);
} finally {
@ -245,7 +246,11 @@
}
}
onMount(async () => {
if (!isPublicRepository) {
await scanRepository();
} else {
scanning = false;
}
});
</script>
@ -262,12 +267,10 @@
</div>
</div>
{:else}
<div class="max-w-7xl mx-auto ">
<div class="title pb-2">Coolify Buildpacks</div>
<div class="max-w-5xl mx-auto ">
<div class="title pb-2">Coolify</div>
<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">
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
</div>
@ -275,10 +278,10 @@
</div>
</div>
<div class="max-w-7xl mx-auto ">
<div class="title pb-2">Heroku</div>
<div class="max-w-5xl mx-auto ">
<div class="title pb-2">Other</div>
<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">
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
</div>

View File

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

View File

@ -31,6 +31,8 @@
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common';
import { appSession } from '$lib/store';
import PublicRepository from './_PublicRepository.svelte';
import Explainer from '$lib/components/Explainer.svelte';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
@ -71,7 +73,9 @@
{$t('application.configuration.select_a_git_source')}
</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}
<div class="flex-col">
<div class="pb-2 text-center font-bold">
@ -188,3 +192,7 @@
</div>
{/if}
</div>
<div class="title py-4">Public Repository</div>
<PublicRepository />
</div>

View File

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

View File

@ -109,7 +109,7 @@
<div class="flex justify-end sticky top-0 p-2 mx-1">
<button
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"
class:text-green-500={followingBuild}
>

View File

@ -147,7 +147,7 @@
<div class="flex justify-end sticky top-0 p-1 mx-1">
<button
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"
class:text-green-500={followingLogs}
>

View File

@ -87,9 +87,7 @@
</div>
<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">
<thead>
<tr class="h-12">
@ -109,4 +107,7 @@
</tr>
</tbody>
</table>
<div class="flex justify-center py-4 text-center">
<Explainer customClass="w-full" text={$t('application.storage.persistent_storage_explainer')} />
</div>
</div>

View File

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