Merged upstream and fixed expose port implementation

This commit is contained in:
Aaron Styles
2022-04-28 21:49:13 +10:00
192 changed files with 5464 additions and 2405 deletions

7
src/app.d.ts vendored
View File

@@ -6,7 +6,11 @@ declare namespace App {
cookies: Record<string, string>;
}
interface Platform {}
interface Session extends SessionData {}
interface Session extends SessionData {
whiteLabelDetails: {
icon: string | null;
};
}
interface Stuff {
service: any;
application: any;
@@ -27,6 +31,7 @@ interface SessionData {
userId?: string | null;
teamId?: string | null;
permission?: string;
lang?: string;
isAdmin?: boolean;
expires?: string | null;
}

View File

@@ -6,8 +6,12 @@ import { getUserDetails, sentry } from '$lib/common';
import { version } from '$lib/common';
import cookie from 'cookie';
import { dev } from '$app/env';
import { locales } from '$lib/translations';
const whiteLabeled = process.env['COOLIFY_WHITE_LABELED'] === 'true';
const whiteLabelDetails = {
icon: (whiteLabeled && process.env['COOLIFY_WHITE_LABELED_ICON']) || null
};
export const handle = handleSession(
{
@@ -17,6 +21,24 @@ export const handle = handleSession(
},
async function ({ event, resolve }) {
let response;
const { url, request } = event;
// Get defined locales
const supportedLocales = locales.get();
let locale;
if (event.locals.cookies['lang']) {
locale = event.locals.cookies['lang'];
} else if (!locale) {
locale = `${`${request.headers.get('accept-language')}`.match(
/[a-zA-Z]+?(?=-|_|,|;)/
)}`.toLowerCase();
}
// Set default locale if user preferred locale does not match
if (!supportedLocales.includes(locale)) locale = 'en';
try {
if (event.locals.cookies) {
if (event.locals.cookies['kit.session']) {
@@ -36,12 +58,14 @@ export const handle = handleSession(
}
response = await resolve(event, {
ssr: !event.url.pathname.startsWith('/webhooks/success')
ssr: !event.url.pathname.startsWith('/webhooks/success'),
transformPage: ({ html }) => html.replace(/<html.*>/, `<html lang="${locale}">`)
});
} catch (error) {
console.log(error);
response = await resolve(event, {
ssr: !event.url.pathname.startsWith('/webhooks/success')
ssr: !event.url.pathname.startsWith('/webhooks/success'),
transformPage: ({ html }) => html.replace(/<html.*>/, `<html lang="${locale}">`)
});
response.headers.append(
'Set-Cookie',
@@ -64,16 +88,27 @@ export const handle = handleSession(
expires: new Date('Thu, 01 Jan 1970 00:00:01 GMT')
})
);
} finally {
return response;
}
response.headers.append(
'Set-Cookie',
cookie.serialize('lang', locale, {
path: '/',
sameSite: 'strict',
maxAge: 30 * 24 * 60 * 60
})
);
return response;
}
);
export const getSession: GetSession = function ({ locals }) {
return {
lang: locals.cookies.lang,
version,
whiteLabeled,
whiteLabelDetails,
...locals.session.data
};
};

View File

@@ -1,9 +1,15 @@
async function send({ method, path, data = {}, headers, timeout = 30000 }) {
async function send({
method,
path,
data = {},
headers,
timeout = 120000
}): Promise<Record<string, unknown>> {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
const opts = { method, headers: {}, body: null, signal: controller.signal };
if (Object.keys(data).length > 0) {
let parsedData = data;
const parsedData = data;
for (const [key, value] of Object.entries(data)) {
if (value === '') {
parsedData[key] = null;
@@ -43,18 +49,33 @@ async function send({ method, path, data = {}, headers, timeout = 30000 }) {
return responseData;
}
export function get(path, headers = {}): Promise<any> {
export function get(
path: string,
headers?: Record<string, unknown>
): Promise<Record<string, unknown>> {
return send({ method: 'GET', path, headers });
}
export function del(path, data = {}, headers = {}): Promise<any> {
export function del(
path: string,
data: Record<string, unknown>,
headers?: Record<string, unknown>
): Promise<Record<string, unknown>> {
return send({ method: 'DELETE', path, data, headers });
}
export function post(path, data, headers = {}): Promise<any> {
export function post(
path: string,
data: Record<string, unknown>,
headers?: Record<string, unknown>
): Promise<Record<string, unknown>> {
return send({ method: 'POST', path, data, headers });
}
export function put(path, data, headers = {}): Promise<any> {
export function put(
path: string,
data: Record<string, unknown>,
headers?: Record<string, unknown>
): Promise<Record<string, unknown>> {
return send({ method: 'PUT', path, data, headers });
}

View File

@@ -91,7 +91,9 @@ export const setDefaultConfiguration = async (data) => {
startCommand,
buildCommand,
publishDirectory,
baseDirectory
baseDirectory,
dockerFileLocation,
denoMainFile
} = data;
const template = scanningTemplates[buildPack];
if (!port) {
@@ -102,14 +104,25 @@ export const setDefaultConfiguration = async (data) => {
else if (buildPack === 'php') port = 80;
else if (buildPack === 'python') port = 8000;
}
if (!installCommand) installCommand = template?.installCommand || 'yarn install';
if (!startCommand) startCommand = template?.startCommand || 'yarn start';
if (!buildCommand) buildCommand = template?.buildCommand || null;
if (!installCommand && buildPack !== 'static')
installCommand = template?.installCommand || 'yarn install';
if (!startCommand && buildPack !== 'static')
startCommand = template?.startCommand || 'yarn start';
if (!buildCommand && buildPack !== 'static') buildCommand = template?.buildCommand || null;
if (!publishDirectory) publishDirectory = template?.publishDirectory || null;
if (baseDirectory) {
if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`;
if (!baseDirectory.endsWith('/')) baseDirectory = `${baseDirectory}/`;
}
if (dockerFileLocation) {
if (!dockerFileLocation.startsWith('/')) dockerFileLocation = `/${dockerFileLocation}`;
if (dockerFileLocation.endsWith('/')) dockerFileLocation = dockerFileLocation.slice(0, -1);
} else {
dockerFileLocation = '/Dockerfile';
}
if (!denoMainFile) {
denoMainFile = 'main.ts';
}
return {
buildPack,
@@ -118,7 +131,9 @@ export const setDefaultConfiguration = async (data) => {
startCommand,
buildCommand,
publishDirectory,
baseDirectory
baseDirectory,
dockerFileLocation,
denoMainFile
};
};
@@ -184,7 +199,11 @@ export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, ap
}
`
);
await saveBuildLog({ line: 'Copied default configuration file.', buildId, applicationId });
await saveBuildLog({
line: 'Copied default configuration file for Nginx.',
buildId,
applicationId
});
}
} catch (error) {
console.log(error);

View File

@@ -0,0 +1,54 @@
import { buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => {
const { workdir, port, baseDirectory, secrets, pullmergeRequestId, denoMainFile, denoOptions } =
data;
const Dockerfile: Array<string> = [];
let depsFound = false;
try {
await fs.readFile(`${workdir}${baseDirectory || ''}/deps.ts`);
depsFound = true;
} catch (error) {}
Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`);
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
}
}
});
}
if (depsFound) {
Dockerfile.push(`COPY .${baseDirectory || ''}/deps.ts /app`);
Dockerfile.push(`RUN deno cache deps.ts`);
}
Dockerfile.push(`COPY ${denoMainFile} /app`);
Dockerfile.push(`RUN deno cache ${denoMainFile}`);
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
Dockerfile.push(`ENV NO_COLOR true`);
Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push(`CMD deno run ${denoOptions ? denoOptions.split(' ') : ''} ${denoMainFile}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
};
export default async function (data) {
try {
const image = 'denoland/deno:latest';
await createDockerfile(data, image);
await buildImage(data);
} catch (error) {
throw error;
}
}

View File

@@ -10,15 +10,16 @@ export default async function ({
buildId,
baseDirectory,
secrets,
pullmergeRequestId
pullmergeRequestId,
dockerFileLocation
}) {
try {
let file = `${workdir}/Dockerfile`;
const file = `${workdir}${dockerFileLocation}`;
let dockerFileOut = `${workdir}`;
if (baseDirectory) {
file = `${workdir}/${baseDirectory}/Dockerfile`;
workdir = `${workdir}/${baseDirectory}`;
dockerFileOut = `${workdir}${baseDirectory}`;
workdir = `${workdir}${baseDirectory}`;
}
const Dockerfile: Array<string> = (await fs.readFile(`${file}`, 'utf8'))
.toString()
.trim()
@@ -26,20 +27,23 @@ export default async function ({
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
if (
(pullmergeRequestId && secret.isPRMRSecret) ||
(!pullmergeRequestId && !secret.isPRMRSecret)
) {
Dockerfile.unshift(`ARG ${secret.name}=${secret.value}`);
Dockerfile.forEach((line, index) => {
if (line.startsWith('FROM')) {
Dockerfile.splice(index + 1, 0, `ARG ${secret.name}`);
}
});
}
}
});
}
await fs.writeFile(`${file}`, Dockerfile.join('\n'));
await buildImage({ applicationId, tag, workdir, docker, buildId, debug });
await fs.writeFile(`${dockerFileOut}${dockerFileLocation}`, Dockerfile.join('\n'));
await buildImage({ applicationId, tag, workdir, docker, buildId, debug, dockerFileLocation });
} catch (error) {
throw error;
}

View File

@@ -13,6 +13,7 @@ import rust from './rust';
import astro from './static';
import eleventy from './static';
import python from './python';
import deno from './deno';
export {
node,
@@ -29,5 +30,6 @@ export {
rust,
astro,
eleventy,
python
python,
deno
};

View File

@@ -12,7 +12,8 @@ import { version as currentVersion } from '../../package.json';
import dayjs from 'dayjs';
import Cookie from 'cookie';
import os from 'os';
import cuid from 'cuid';
import type { RequestEvent } from '@sveltejs/kit/types/internal';
import type { Job } from 'bullmq';
try {
if (!dev) {
@@ -25,7 +26,7 @@ try {
initialScope: {
tags: {
appId: process.env['COOLIFY_APP_ID'],
'os.arch': os.arch(),
'os.arch': getOsArch(),
'os.platform': os.platform(),
'os.release': os.release()
}
@@ -45,37 +46,30 @@ const customConfig: Config = {
export const version = currentVersion;
export const asyncExecShell = util.promisify(child.exec);
export const asyncSleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay));
export const asyncSleep = (delay: number): Promise<unknown> =>
new Promise((resolve) => setTimeout(resolve, delay));
export const sentry = Sentry;
export const uniqueName = () => uniqueNamesGenerator(customConfig);
export const uniqueName = (): string => uniqueNamesGenerator(customConfig);
export const saveBuildLog = async ({ line, buildId, applicationId }) => {
if (line) {
if (line.includes('ghs_')) {
const regex = /ghs_.*@/g;
line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
}
const addTimestamp = `${generateTimestamp()} ${line}`;
return await buildLogQueue.add(buildId, { buildId, line: addTimestamp, applicationId });
export const saveBuildLog = async ({
line,
buildId,
applicationId
}: {
line: string;
buildId: string;
applicationId: string;
}): Promise<Job> => {
if (line && typeof line === 'string' && line.includes('ghs_')) {
const regex = /ghs_.*@/g;
line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
}
const addTimestamp = `${generateTimestamp()} ${line}`;
return await buildLogQueue.add(buildId, { buildId, line: addTimestamp, applicationId });
};
export const isTeamIdTokenAvailable = (request) => {
const cookie = request.headers.cookie
?.split(';')
.map((s) => s.trim())
.find((s) => s.startsWith('teamId='))
?.split('=')[1];
if (!cookie) {
return getTeam(request);
} else {
return cookie;
}
};
export const getTeam = (event) => {
export const getTeam = (event: RequestEvent): string | null => {
const cookies = Cookie.parse(event.request.headers.get('cookie'));
if (cookies?.teamId) {
return cookies.teamId;
@@ -85,14 +79,28 @@ export const getTeam = (event) => {
return null;
};
export const getUserDetails = async (event, isAdminRequired = true) => {
export const getUserDetails = async (
event: RequestEvent,
isAdminRequired = true
): Promise<{
teamId: string;
userId: string;
permission: string;
status: number;
body: { message: string };
}> => {
const teamId = getTeam(event);
const userId = event?.locals?.session?.data?.userId || null;
const { permission = 'read' } = await db.prisma.permission.findFirst({
where: { teamId, userId },
select: { permission: true },
rejectOnNotFound: true
});
let permission = 'read';
if (teamId && userId) {
const data = await db.prisma.permission.findFirst({
where: { teamId, userId },
select: { permission: true },
rejectOnNotFound: true
});
if (data.permission) permission = data.permission;
}
const payload = {
teamId,
userId,
@@ -112,11 +120,11 @@ export const getUserDetails = async (event, isAdminRequired = true) => {
return payload;
};
export function getEngine(engine) {
export function getEngine(engine: string): string {
return engine === '/var/run/docker.sock' ? 'unix:///var/run/docker.sock' : engine;
}
export async function removeContainer(id, engine) {
export async function removeContainer(id: string, engine: string): Promise<void> {
const host = getEngine(engine);
try {
const { stdout } = await asyncExecShell(
@@ -132,11 +140,23 @@ export async function removeContainer(id, engine) {
}
}
export const removeDestinationDocker = async ({ id, engine }) => {
export const removeDestinationDocker = async ({
id,
engine
}: {
id: string;
engine: string;
}): Promise<void> => {
return await removeContainer(id, engine);
};
export const createDirectories = async ({ repository, buildId }) => {
export const createDirectories = async ({
repository,
buildId
}: {
repository: string;
buildId: string;
}): Promise<{ workdir: string; repodir: string }> => {
const repodir = `/tmp/build-sources/${repository}/`;
const workdir = `/tmp/build-sources/${repository}/${buildId}`;
@@ -148,20 +168,14 @@ export const createDirectories = async ({ repository, buildId }) => {
};
};
export function generateTimestamp() {
export function generateTimestamp(): string {
return `${dayjs().format('HH:mm:ss.SSS')} `;
}
export function getDomain(domain) {
export function getDomain(domain: string): string {
return domain?.replace('https://', '').replace('http://', '');
}
export function dashify(str: string, options?: any): string {
if (typeof str !== 'string') return str;
return str
.trim()
.replace(/\W/g, (m) => (/[À-ž]/.test(m) ? m : '-'))
.replace(/^-+|-+$/g, '')
.replace(/-{2,}/g, (m) => (options && options.condense ? '-' : m))
.toLowerCase();
export function getOsArch() {
return os.arch();
}

View File

@@ -6,6 +6,7 @@
import N8n from './svg/services/N8n.svelte';
import NocoDb from './svg/services/NocoDB.svelte';
import PlausibleAnalytics from './svg/services/PlausibleAnalytics.svelte';
import Umami from './svg/services/Umami.svelte';
import UptimeKuma from './svg/services/UptimeKuma.svelte';
import VaultWarden from './svg/services/VaultWarden.svelte';
import VsCodeServer from './svg/services/VSCodeServer.svelte';
@@ -52,4 +53,8 @@
<a href="https://ghost.org" target="_blank">
<Ghost />
</a>
{:else if service.type === 'umami'}
<a href="https://umami.is" target="_blank">
<Umami />
</a>
{/if}

View File

@@ -19,7 +19,7 @@ export const staticDeployments = [
'astro',
'eleventy'
];
export const notNodeDeployments = ['php', 'docker', 'rust', 'python'];
export const notNodeDeployments = ['php', 'docker', 'rust', 'python', 'deno'];
export function getDomain(domain) {
return domain?.replace('https://', '').replace('http://', '');
@@ -180,5 +180,16 @@ export const supportedServiceTypesAndVersions = [
ports: {
main: 7700
}
},
{
name: 'umami',
fancyName: 'Umami',
baseImage: 'ghcr.io/mikecao/umami',
images: ['postgres:12-alpine'],
versions: ['postgresql-latest'],
recommendedVersion: 'postgresql-latest',
ports: {
main: 3000
}
}
];

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@@ -0,0 +1,85 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<svg
version="1.0"
xmlns="http://www.w3.org/2000/svg"
width="856.000000pt"
height="856.000000pt"
viewBox="0 0 856.000000 856.000000"
preserveAspectRatio="xMidYMid meet"
class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 mx-auto'}
>
<metadata> Created by potrace 1.11, written by Peter Selinger 2001-2013 </metadata>
<g
transform="translate(0.000000,856.000000) scale(0.100000,-0.100000)"
fill="currentColor"
stroke="none"
>
<path
d="M4027 8163 c-2 -2 -28 -5 -58 -7 -50 -4 -94 -9 -179 -22 -19 -2 -48
-6 -65 -9 -47 -6 -236 -44 -280 -55 -22 -6 -49 -12 -60 -15 -34 -6 -58 -13
-130 -36 -38 -13 -72 -23 -75 -24 -29 -6 -194 -66 -264 -96 -49 -22 -95 -39
-102 -39 -7 0 -19 -7 -28 -15 -8 -9 -18 -15 -21 -14 -7 1 -197 -92 -205 -101
-3 -3 -21 -13 -40 -24 -79 -42 -123 -69 -226 -137 -94 -62 -246 -173 -280
-204 -6 -5 -29 -25 -52 -43 -136 -111 -329 -305 -457 -462 -21 -25 -41 -47
-44 -50 -4 -3 -22 -26 -39 -52 -18 -25 -38 -52 -45 -60 -34 -35 -207 -308
-259 -408 -13 -25 -25 -47 -28 -50 -11 -11 -121 -250 -159 -346 -42 -105 -114
-321 -126 -374 l-7 -30 -263 0 c-245 0 -268 -2 -321 -21 -94 -35 -171 -122
-191 -216 -9 -39 -8 -852 0 -938 9 -87 16 -150 23 -195 3 -19 6 -48 8 -65 3
-29 14 -97 22 -140 3 -11 7 -36 10 -55 3 -19 9 -51 14 -70 5 -19 11 -46 14
-60 29 -138 104 -401 145 -505 5 -11 23 -58 42 -105 18 -47 42 -105 52 -130
11 -25 21 -49 22 -55 3 -10 109 -224 164 -330 18 -33 50 -89 71 -124 22 -34
40 -64 40 -66 0 -8 104 -161 114 -167 6 -4 7 -8 3 -8 -4 0 4 -12 18 -27 14
-15 25 -32 25 -36 0 -5 6 -14 13 -21 6 -7 21 -25 32 -41 11 -15 34 -44 50 -64
17 -21 41 -52 55 -70 13 -18 33 -43 45 -56 11 -13 42 -49 70 -81 100 -118 359
-369 483 -469 34 -27 62 -53 62 -57 0 -5 6 -8 13 -8 7 0 19 -9 27 -20 8 -11
19 -20 26 -20 6 0 19 -9 29 -20 10 -11 22 -20 27 -20 5 0 23 -13 41 -30 18
-16 37 -30 44 -30 6 0 13 -4 15 -8 3 -8 186 -132 194 -132 2 0 27 -15 56 -34
132 -83 377 -207 558 -280 36 -15 74 -31 85 -36 62 -26 220 -81 320 -109 79
-23 191 -53 214 -57 14 -3 28 -7 31 -9 4 -2 20 -7 36 -9 16 -3 40 -8 54 -11
14 -3 36 -8 50 -11 14 -2 36 -7 50 -10 13 -3 40 -8 60 -10 19 -2 46 -7 60 -10
54 -10 171 -25 320 -40 90 -9 613 -12 636 -4 11 5 28 4 37 -1 9 -6 17 -6 17
-1 0 4 10 8 23 9 29 0 154 12 192 18 17 3 46 7 65 9 70 10 131 20 183 32 16 3
38 7 50 9 45 7 165 36 252 60 50 14 100 28 112 30 12 3 34 10 48 15 14 5 25 7
25 4 0 -4 6 -2 13 3 6 6 30 16 52 22 22 7 47 15 55 18 8 4 17 7 20 7 10 2 179
68 240 94 96 40 342 159 395 191 17 10 53 30 80 46 28 15 81 47 118 71 37 24
72 44 76 44 5 0 11 3 13 8 2 4 30 25 63 47 33 22 62 42 65 45 3 3 50 38 105
79 55 40 105 79 110 85 6 6 24 22 40 34 85 65 465 430 465 447 0 3 8 13 18 23
9 10 35 40 57 66 22 27 47 56 55 65 8 9 42 52 74 96 32 44 71 96 85 115 140
183 358 576 461 830 12 30 28 69 36 85 24 56 123 355 117 355 -3 0 -1 6 5 13
6 6 14 30 18 52 10 48 9 46 17 65 5 13 37 155 52 230 9 42 35 195 40 231 34
235 40 357 40 804 l0 420 -24 44 c-46 87 -143 157 -231 166 -19 2 -144 4 -276
4 l-242 1 -36 118 c-21 64 -46 139 -56 166 -11 27 -20 52 -20 57 0 5 -11 33
-25 63 -14 30 -25 58 -25 61 0 18 -152 329 -162 333 -5 2 -8 10 -8 18 0 8 -4
14 -10 14 -5 0 -9 3 -8 8 3 9 -40 82 -128 217 -63 97 -98 145 -187 259 -133
171 -380 420 -559 564 -71 56 -132 102 -138 102 -5 0 -10 3 -10 8 0 4 -25 23
-55 42 -30 19 -55 38 -55 43 0 4 -6 7 -13 7 -7 0 -22 8 -33 18 -11 9 -37 26
-59 37 -21 11 -44 25 -50 30 -41 37 -413 220 -540 266 -27 9 -61 22 -75 27
-14 5 -28 10 -32 11 -4 1 -28 10 -53 21 -25 11 -46 19 -48 18 -2 -1 -109 29
-137 40 -13 4 -32 9 -65 16 -5 1 -16 5 -22 9 -7 5 -13 6 -13 3 0 -2 -15 0 -32
5 -18 5 -44 11 -58 14 -14 3 -36 7 -50 10 -14 3 -50 9 -80 15 -30 6 -64 12
-75 14 -11 2 -45 6 -75 10 -30 4 -71 9 -90 12 -19 3 -53 6 -75 7 -22 1 -44 5
-50 8 -11 7 -542 9 -548 2z m57 -404 c7 10 436 8 511 -3 22 -3 60 -8 85 -11
25 -2 56 -6 70 -9 14 -2 43 -7 65 -10 38 -5 58 -9 115 -21 14 -3 34 -7 45 -9
11 -2 58 -14 105 -26 47 -12 92 -23 100 -25 35 -7 279 -94 308 -109 17 -9 34
-16 37 -16 3 1 20 -6 38 -14 17 -8 68 -31 112 -51 44 -20 82 -35 84 -35 2 1 7
-3 10 -8 3 -5 43 -28 88 -51 45 -23 87 -48 93 -56 7 -8 17 -15 22 -15 12 0
192 -121 196 -132 2 -4 8 -8 13 -8 10 0 119 -86 220 -172 102 -87 256 -244
349 -357 25 -30 53 -63 63 -73 9 -10 17 -22 17 -28 0 -5 3 -10 8 -10 4 0 25
-27 46 -60 22 -33 43 -60 48 -60 4 0 8 -5 8 -11 0 -6 11 -25 25 -43 14 -18 25
-38 25 -44 0 -7 4 -12 8 -12 5 0 16 -15 25 -32 9 -18 30 -55 47 -83 46 -77
161 -305 154 -305 -4 0 -2 -6 4 -12 6 -7 23 -47 40 -88 16 -41 33 -84 37 -95
5 -11 9 -22 10 -25 0 -3 11 -36 24 -73 13 -38 21 -70 19 -73 -3 -2 -1386 -3
-3075 -2 l-3071 3 38 110 c47 137 117 301 182 425 62 118 167 295 191 320 9
11 17 22 17 25 0 7 39 63 58 83 6 7 26 35 44 60 18 26 37 52 43 57 6 6 34 37
61 70 48 59 271 286 329 335 17 14 53 43 80 65 28 22 52 42 55 45 3 3 21 17
40 30 19 14 40 28 45 32 40 32 105 78 109 78 3 0 28 16 55 35 26 19 53 35 58
35 5 0 18 8 29 18 17 15 53 35 216 119 118 60 412 176 422 166 3 -4 6 -2 6 4
0 6 12 13 28 16 15 3 52 12 82 21 30 9 63 19 73 21 10 2 27 7 37 10 10 3 29 8
42 10 13 3 48 10 78 16 30 7 61 12 68 12 6 0 12 4 12 9 0 5 5 6 10 3 6 -4 34
-2 63 4 51 11 71 13 197 26 36 4 67 9 69 11 2 2 10 -1 17 -7 8 -6 14 -7 18 0z"
/>
</g>
</svg>

View File

@@ -153,6 +153,16 @@ export function findBuildPack(pack, packageManager = 'npm') {
port: 8000
};
}
if (pack === 'deno') {
return {
...metaData,
installCommand: null,
buildCommand: null,
startCommand: null,
publishDirectory: null,
port: 8000
};
}
return {
name: 'node',
fancyName: 'Node.js',
@@ -262,6 +272,12 @@ export const buildPacks = [
fancyName: 'Python',
hoverColor: 'hover:bg-green-700',
color: 'bg-green-700'
},
{
name: 'deno',
fancyName: 'Deno',
hoverColor: 'hover:bg-green-700',
color: 'bg-green-700'
}
];
export const scanningTemplates = {

View File

@@ -1,13 +1,13 @@
import crypto from 'crypto';
const algorithm = 'aes-256-ctr';
export const base64Encode = (text: string) => {
export const base64Encode = (text: string): string => {
return Buffer.from(text).toString('base64');
};
export const base64Decode = (text: string) => {
export const base64Decode = (text: string): string => {
return Buffer.from(text, 'base64').toString('ascii');
};
export const encrypt = (text: string) => {
export const encrypt = (text: string): string => {
if (text) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, process.env['COOLIFY_SECRET_KEY'], iv);
@@ -19,7 +19,7 @@ export const encrypt = (text: string) => {
}
};
export const decrypt = (hashString: string) => {
export const decrypt = (hashString: string): string => {
if (hashString) {
const hash: Hash = JSON.parse(hashString);
const decipher = crypto.createDecipheriv(

View File

@@ -1,10 +1,19 @@
import { decrypt, encrypt } from '$lib/crypto';
import { asyncExecShell, getEngine } from '$lib/common';
import { getDomain, removeDestinationDocker } from '$lib/common';
import { removeDestinationDocker } from '$lib/common';
import { prisma } from './common';
export async function listApplications(teamId) {
import type {
DestinationDocker,
GitSource,
Secret,
ApplicationSettings,
Application,
ApplicationPersistentStorage
} from '@prisma/client';
export async function listApplications(teamId: string): Promise<Application[]> {
if (teamId === '0') {
return await prisma.application.findMany({ include: { teams: true } });
}
@@ -14,7 +23,13 @@ export async function listApplications(teamId) {
});
}
export async function newApplication({ name, teamId }) {
export async function newApplication({
name,
teamId
}: {
name: string;
teamId: string;
}): Promise<Application> {
return await prisma.application.create({
data: {
name,
@@ -24,34 +39,17 @@ export async function newApplication({ name, teamId }) {
});
}
export async function importApplication({
name,
teamId,
fqdn,
port,
buildCommand,
startCommand,
installCommand
}) {
return await prisma.application.create({
data: {
name,
fqdn,
port,
buildCommand,
startCommand,
installCommand,
teams: { connect: { id: teamId } }
}
});
}
export async function removeApplication({ id, teamId }) {
const { fqdn, destinationDockerId, destinationDocker } = await prisma.application.findUnique({
export async function removeApplication({
id,
teamId
}: {
id: string;
teamId: string;
}): Promise<void> {
const { destinationDockerId, destinationDocker } = await prisma.application.findUnique({
where: { id },
include: { destinationDocker: true }
});
const domain = getDomain(fqdn);
if (destinationDockerId) {
const host = getEngine(destinationDocker.engine);
const { stdout: containers } = await asyncExecShell(
@@ -62,7 +60,6 @@ export async function removeApplication({ id, teamId }) {
for (const container of containersArray) {
const containerObj = JSON.parse(container);
const id = containerObj.ID;
const preview = containerObj.Image.split('-')[1];
await removeDestinationDocker({ id, engine: destinationDocker.engine });
}
}
@@ -80,9 +77,23 @@ export async function removeApplication({ id, teamId }) {
}
}
export async function getApplicationWebhook({ projectId, branch }) {
export async function getApplicationWebhook({
projectId,
branch
}: {
projectId: number;
branch: string;
}): Promise<
Application & {
destinationDocker: DestinationDocker;
settings: ApplicationSettings;
gitSource: GitSource;
secrets: Secret[];
persistentStorage: ApplicationPersistentStorage[];
}
> {
try {
let application = await prisma.application.findFirst({
const application = await prisma.application.findFirst({
where: { projectId, branch, settings: { autodeploy: true } },
include: {
destinationDocker: true,
@@ -131,16 +142,17 @@ export async function getApplicationWebhook({ projectId, branch }) {
throw { status: 404, body: { message: e.message } };
}
}
export async function getApplicationById({ id }) {
const body = await prisma.application.findFirst({
where: { id },
include: { destinationDocker: true }
});
return { ...body };
}
export async function getApplication({ id, teamId }) {
let body = {};
export async function getApplication({ id, teamId }: { id: string; teamId: string }): Promise<
Application & {
destinationDocker: DestinationDocker;
settings: ApplicationSettings;
gitSource: GitSource;
secrets: Secret[];
persistentStorage: ApplicationPersistentStorage[];
}
> {
let body;
if (teamId === '0') {
body = await prisma.application.findFirst({
where: { id },
@@ -194,7 +206,14 @@ export async function configureGitRepository({
projectId,
webhookToken,
autodeploy
}) {
}: {
id: string;
repository: string;
branch: string;
projectId: number;
webhookToken: string;
autodeploy: boolean;
}): Promise<void> {
if (webhookToken) {
const encryptedWebhookToken = encrypt(webhookToken);
await prisma.application.update({
@@ -224,7 +243,10 @@ export async function configureGitRepository({
}
}
export async function configureBuildPack({ id, buildPack }) {
export async function configureBuildPack({
id,
buildPack
}: Pick<Application, 'id' | 'buildPack'>): Promise<Application> {
return await prisma.application.update({ where: { id }, data: { buildPack } });
}
@@ -242,8 +264,29 @@ export async function configureApplication({
publishDirectory,
pythonWSGI,
pythonModule,
pythonVariable
}) {
pythonVariable,
dockerFileLocation,
denoMainFile,
denoOptions
}: {
id: string;
buildPack: string;
name: string;
fqdn: string;
port: number;
exposePort: number;
installCommand: string;
buildCommand: string;
startCommand: string;
baseDirectory: string;
publishDirectory: string;
pythonWSGI: string;
pythonModule: string;
pythonVariable: string;
dockerFileLocation: string;
denoMainFile: string;
denoOptions: string;
}): Promise<Application> {
return await prisma.application.update({
where: { id },
data: {
@@ -259,16 +302,32 @@ export async function configureApplication({
publishDirectory,
pythonWSGI,
pythonModule,
pythonVariable
pythonVariable,
dockerFileLocation,
denoMainFile,
denoOptions
}
});
}
export async function checkDoubleBranch(branch, projectId) {
export async function checkDoubleBranch(branch: string, projectId: number): Promise<boolean> {
const applications = await prisma.application.findMany({ where: { branch, projectId } });
return applications.length > 1;
}
export async function setApplicationSettings({ id, debug, previews, dualCerts, autodeploy }) {
export async function setApplicationSettings({
id,
debug,
previews,
dualCerts,
autodeploy
}: {
id: string;
debug: boolean;
previews: boolean;
dualCerts: boolean;
autodeploy: boolean;
}): Promise<Application & { destinationDocker: DestinationDocker }> {
return await prisma.application.update({
where: { id },
data: { settings: { update: { debug, previews, dualCerts, autodeploy } } },
@@ -276,29 +335,6 @@ export async function setApplicationSettings({ id, debug, previews, dualCerts, a
});
}
export async function createBuild({
id,
applicationId,
destinationDockerId,
gitSourceId,
githubAppId,
gitlabAppId,
type
}) {
return await prisma.build.create({
data: {
id,
applicationId,
destinationDockerId,
gitSourceId,
githubAppId,
gitlabAppId,
status: 'running',
type
}
});
}
export async function getPersistentStorage(id) {
export async function getPersistentStorage(id: string): Promise<ApplicationPersistentStorage[]> {
return await prisma.applicationPersistentStorage.findMany({ where: { applicationId: id } });
}

View File

@@ -1,7 +1,16 @@
import { getDomain } from '$lib/common';
import { prisma } from './common';
import type { Application, ServiceSecret, DestinationDocker, Secret } from '@prisma/client';
export async function isBranchAlreadyUsed({ repository, branch, id }) {
export async function isBranchAlreadyUsed({
repository,
branch,
id
}: {
id: string;
repository: string;
branch: string;
}): Promise<Application> {
const application = await prisma.application.findUnique({
where: { id },
include: { gitSource: true }
@@ -11,18 +20,42 @@ export async function isBranchAlreadyUsed({ repository, branch, id }) {
});
}
export async function isDockerNetworkExists({ network }) {
export async function isDockerNetworkExists({
network
}: {
network: string;
}): Promise<DestinationDocker> {
return await prisma.destinationDocker.findFirst({ where: { network } });
}
export async function isServiceSecretExists({ id, name }) {
export async function isServiceSecretExists({
id,
name
}: {
id: string;
name: string;
}): Promise<ServiceSecret> {
return await prisma.serviceSecret.findFirst({ where: { name, serviceId: id } });
}
export async function isSecretExists({ id, name, isPRMRSecret }) {
export async function isSecretExists({
id,
name,
isPRMRSecret
}: {
id: string;
name: string;
isPRMRSecret: boolean;
}): Promise<Secret> {
return await prisma.secret.findFirst({ where: { name, applicationId: id, isPRMRSecret } });
}
export async function isDomainConfigured({ id, fqdn }) {
export async function isDomainConfigured({
id,
fqdn
}: {
id: string;
fqdn: string;
}): Promise<boolean> {
const domain = getDomain(fqdn);
const nakedDomain = domain.replace('www.', '');
const foundApp = await prisma.application.findFirst({
@@ -55,6 +88,5 @@ export async function isDomainConfigured({ id, fqdn }) {
},
select: { fqdn: true }
});
if (foundApp || foundService || coolifyFqdn) return true;
return false;
return !!(foundApp || foundService || coolifyFqdn);
}

View File

@@ -6,11 +6,12 @@ import {
} from '$lib/components/common';
import * as Prisma from '@prisma/client';
import { default as ProdPrisma } from '@prisma/client';
import type { PrismaClientOptions } from '@prisma/client/runtime';
import type { Database, DatabaseSettings } from '@prisma/client';
import generator from 'generate-password';
import forge from 'node-forge';
import getPort, { portNumbers } from 'get-port';
export function generatePassword(length = 24) {
export function generatePassword(length = 24): string {
return generator.generate({
length,
numbers: true,
@@ -30,8 +31,14 @@ export const prisma = new PrismaClient({
rejectOnNotFound: false
});
export function ErrorHandler(e) {
if (e! instanceof Error) {
export function ErrorHandler(e: {
stdout?;
message?: string;
status?: number;
name?: string;
error?: string;
}): { status: number; body: { message: string; error: string } } {
if (e && e instanceof Error) {
e = new Error(e.toString());
}
let truncatedError = e;
@@ -39,8 +46,7 @@ export function ErrorHandler(e) {
truncatedError = e.stdout;
}
if (e.message?.includes('docker run')) {
let truncatedArray = [];
truncatedArray = truncatedError.message.split('-').filter((line) => {
const truncatedArray: string[] = truncatedError.message.split('-').filter((line) => {
if (!line.startsWith('e ')) {
return line;
}
@@ -68,11 +74,11 @@ export function ErrorHandler(e) {
payload.body.message = 'Already exists. Choose another name.';
}
}
// console.error(e)
return payload;
}
export async function generateSshKeyPair(): Promise<{ publicKey: string; privateKey: string }> {
return await new Promise(async (resolve, reject) => {
return await new Promise((resolve, reject) => {
forge.pki.rsa.generateKeyPair({ bits: 4096, workers: -1 }, function (err, keys) {
if (keys) {
resolve({
@@ -86,35 +92,94 @@ export async function generateSshKeyPair(): Promise<{ publicKey: string; private
});
}
export function getVersions(type) {
export function getVersions(type: string): string[] {
const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type);
if (found) {
return found.versions;
}
return [];
}
export function getDatabaseImage(type) {
export function getDatabaseImage(type: string): string {
const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type);
if (found) {
return found.baseImage;
}
return '';
}
export function getServiceImage(type) {
export function getServiceImage(type: string): string {
const found = supportedServiceTypesAndVersions.find((t) => t.name === type);
if (found) {
return found.baseImage;
}
return '';
}
export function getServiceImages(type) {
export function getServiceImages(type: string): string[] {
const found = supportedServiceTypesAndVersions.find((t) => t.name === type);
if (found) {
return found.images;
}
return [];
}
export function generateDatabaseConfiguration(database) {
export function generateDatabaseConfiguration(database: Database & { settings: DatabaseSettings }):
| {
volume: string;
image: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
MYSQL_DATABASE: string;
MYSQL_PASSWORD: string;
MYSQL_ROOT_USER: string;
MYSQL_USER: string;
MYSQL_ROOT_PASSWORD: string;
};
}
| {
volume: string;
image: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
MONGODB_ROOT_USER: string;
MONGODB_ROOT_PASSWORD: string;
};
}
| {
volume: string;
image: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
POSTGRESQL_POSTGRES_PASSWORD: string;
POSTGRESQL_USERNAME: string;
POSTGRESQL_PASSWORD: string;
POSTGRESQL_DATABASE: string;
};
}
| {
volume: string;
image: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
REDIS_AOF_ENABLED: string;
REDIS_PASSWORD: string;
};
}
| {
volume: string;
image: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
COUCHDB_PASSWORD: string;
COUCHDB_USER: string;
};
} {
const {
id,
dbUser,
@@ -129,7 +194,6 @@ export function generateDatabaseConfiguration(database) {
const baseImage = getDatabaseImage(type);
if (type === 'mysql') {
return {
// url: `mysql://${dbUser}:${dbUserPassword}@${id}:${isPublic ? port : 3306}/${defaultDatabase}`,
privatePort: 3306,
environmentVariables: {
MYSQL_USER: dbUser,
@@ -144,7 +208,6 @@ export function generateDatabaseConfiguration(database) {
};
} else if (type === 'mongodb') {
return {
// url: `mongodb://${dbUser}:${dbUserPassword}@${id}:${isPublic ? port : 27017}/${defaultDatabase}`,
privatePort: 27017,
environmentVariables: {
MONGODB_ROOT_USER: rootUser,
@@ -156,7 +219,6 @@ export function generateDatabaseConfiguration(database) {
};
} else if (type === 'postgresql') {
return {
// url: `psql://${dbUser}:${dbUserPassword}@${id}:${isPublic ? port : 5432}/${defaultDatabase}`,
privatePort: 5432,
environmentVariables: {
POSTGRESQL_POSTGRES_PASSWORD: rootUserPassword,
@@ -170,7 +232,6 @@ export function generateDatabaseConfiguration(database) {
};
} else if (type === 'redis') {
return {
// url: `redis://${dbUser}:${dbUserPassword}@${id}:${isPublic ? port : 6379}/${defaultDatabase}`,
privatePort: 6379,
environmentVariables: {
REDIS_PASSWORD: dbUserPassword,
@@ -182,7 +243,6 @@ export function generateDatabaseConfiguration(database) {
};
} else if (type === 'couchdb') {
return {
// url: `couchdb://${dbUser}:${dbUserPassword}@${id}:${isPublic ? port : 5984}/${defaultDatabase}`,
privatePort: 5984,
environmentVariables: {
COUCHDB_PASSWORD: dbUserPassword,
@@ -193,18 +253,30 @@ export function generateDatabaseConfiguration(database) {
ulimits: {}
};
}
// } else if (type === 'clickhouse') {
// return {
// url: `clickhouse://${dbUser}:${dbUserPassword}@${id}:${port}/${defaultDatabase}`,
// privatePort: 9000,
// image: `bitnami/clickhouse-server:${version}`,
// volume: `${id}-${type}-data:/var/lib/clickhouse`,
// ulimits: {
// nofile: {
// soft: 262144,
// hard: 262144
// }
// }
// }
// }
}
export async function getFreePort() {
const data = await prisma.setting.findFirst();
const { minPort, maxPort } = data;
const dbUsed = await (
await prisma.database.findMany({
where: { publicPort: { not: null } },
select: { publicPort: true }
})
).map((a) => a.publicPort);
const wpFtpUsed = await (
await prisma.wordpress.findMany({
where: { ftpPublicPort: { not: null } },
select: { ftpPublicPort: true }
})
).map((a) => a.ftpPublicPort);
const wpUsed = await (
await prisma.wordpress.findMany({
where: { mysqlPublicPort: { not: null } },
select: { mysqlPublicPort: true }
})
).map((a) => a.mysqlPublicPort);
const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed];
return await getPort({ port: portNumbers(minPort, maxPort), exclude: usedPorts });
}

View File

@@ -1,12 +1,11 @@
import { decrypt, encrypt } from '$lib/crypto';
import * as db from '$lib/database';
import cuid from 'cuid';
import { generatePassword } from '.';
import { prisma, ErrorHandler } from './common';
import getPort, { portNumbers } from 'get-port';
import { prisma } from './common';
import { asyncExecShell, getEngine, removeContainer } from '$lib/common';
import type { Database, DatabaseSettings, DestinationDocker } from '@prisma/client';
export async function listDatabases(teamId) {
export async function listDatabases(teamId: string): Promise<Database[]> {
if (teamId === '0') {
return await prisma.database.findMany({ include: { teams: true } });
} else {
@@ -16,7 +15,14 @@ export async function listDatabases(teamId) {
});
}
}
export async function newDatabase({ name, teamId }) {
export async function newDatabase({
name,
teamId
}: {
name: string;
teamId: string;
}): Promise<Database> {
const dbUser = cuid();
const dbUserPassword = encrypt(generatePassword());
const rootUser = cuid();
@@ -37,8 +43,14 @@ export async function newDatabase({ name, teamId }) {
});
}
export async function getDatabase({ id, teamId }) {
let body = {};
export async function getDatabase({
id,
teamId
}: {
id: string;
teamId: string;
}): Promise<Database & { destinationDocker: DestinationDocker; settings: DatabaseSettings }> {
let body;
if (teamId === '0') {
body = await prisma.database.findFirst({
where: { id },
@@ -50,20 +62,25 @@ export async function getDatabase({ id, teamId }) {
include: { destinationDocker: true, settings: true }
});
}
if (body.dbUserPassword) body.dbUserPassword = decrypt(body.dbUserPassword);
if (body.rootUserPassword) body.rootUserPassword = decrypt(body.rootUserPassword);
return { ...body };
return body;
}
export async function removeDatabase({ id }) {
export async function removeDatabase({ id }: { id: string }): Promise<void> {
await prisma.databaseSettings.deleteMany({ where: { databaseId: id } });
await prisma.database.delete({ where: { id } });
return;
}
export async function configureDatabaseType({ id, type }) {
export async function configureDatabaseType({
id,
type
}: {
id: string;
type: string;
}): Promise<Database> {
return await prisma.database.update({
where: { id },
data: { type }
@@ -79,7 +96,7 @@ export async function setDatabase({
version?: string;
isPublic?: boolean;
appendOnly?: boolean;
}) {
}): Promise<Database> {
return await prisma.database.update({
where: { id },
data: {
@@ -97,7 +114,16 @@ export async function updateDatabase({
rootUser,
rootUserPassword,
version
}) {
}: {
id: string;
name: string;
defaultDatabase: string;
dbUser: string;
dbUserPassword: string;
rootUser: string;
rootUserPassword: string;
version: string;
}): Promise<Database> {
const encryptedDbUserPassword = dbUserPassword && encrypt(dbUserPassword);
const encryptedRootUserPassword = rootUserPassword && encrypt(rootUserPassword);
return await prisma.database.update({
@@ -114,7 +140,9 @@ export async function updateDatabase({
});
}
export async function stopDatabase(database) {
export async function stopDatabase(
database: Database & { destinationDocker: DestinationDocker }
): Promise<boolean> {
let everStarted = false;
const {
id,

View File

@@ -1,11 +1,22 @@
import { asyncExecShell, getEngine } from '$lib/common';
import { decrypt, encrypt } from '$lib/crypto';
import { dockerInstance } from '$lib/docker';
import { startCoolifyProxy } from '$lib/haproxy';
import { getDatabaseImage } from '.';
import { prisma } from './common';
import type { DestinationDocker, Service, Application, Prisma } from '@prisma/client';
import type { CreateDockerDestination } from '$lib/types/destinations';
export async function listDestinations(teamId) {
type DestinationConfigurationObject = {
id: string;
destinationId: string;
};
type FindDestinationFromTeam = {
id: string;
teamId: string;
};
export async function listDestinations(teamId: string): Promise<DestinationDocker[]> {
if (teamId === '0') {
return await prisma.destinationDocker.findMany({ include: { teams: true } });
}
@@ -15,19 +26,28 @@ export async function listDestinations(teamId) {
});
}
export async function configureDestinationForService({ id, destinationId }) {
export async function configureDestinationForService({
id,
destinationId
}: DestinationConfigurationObject): Promise<Service> {
return await prisma.service.update({
where: { id },
data: { destinationDocker: { connect: { id: destinationId } } }
});
}
export async function configureDestinationForApplication({ id, destinationId }) {
export async function configureDestinationForApplication({
id,
destinationId
}: DestinationConfigurationObject): Promise<Application> {
return await prisma.application.update({
where: { id },
data: { destinationDocker: { connect: { id: destinationId } } }
});
}
export async function configureDestinationForDatabase({ id, destinationId }) {
export async function configureDestinationForDatabase({
id,
destinationId
}: DestinationConfigurationObject): Promise<void> {
await prisma.database.update({
where: { id },
data: { destinationDocker: { connect: { id: destinationId } } }
@@ -48,7 +68,12 @@ export async function configureDestinationForDatabase({ id, destinationId }) {
}
}
}
export async function updateDestination({ id, name, engine, network }) {
export async function updateDestination({
id,
name,
engine,
network
}: Pick<DestinationDocker, 'id' | 'name' | 'engine' | 'network'>): Promise<DestinationDocker> {
return await prisma.destinationDocker.update({ where: { id }, data: { name, engine, network } });
}
@@ -58,13 +83,8 @@ export async function newRemoteDestination({
engine,
network,
isCoolifyProxyUsed,
remoteEngine,
ipAddress,
user,
port,
sshPrivateKey
}) {
const encryptedPrivateKey = encrypt(sshPrivateKey);
remoteEngine
}: CreateDockerDestination): Promise<string> {
const destination = await prisma.destinationDocker.create({
data: {
name,
@@ -72,16 +92,18 @@ export async function newRemoteDestination({
engine,
network,
isCoolifyProxyUsed,
remoteEngine,
ipAddress,
user,
port,
sshPrivateKey: encryptedPrivateKey
remoteEngine
}
});
return destination.id;
}
export async function newLocalDestination({ name, teamId, engine, network, isCoolifyProxyUsed }) {
export async function newLocalDestination({
name,
teamId,
engine,
network,
isCoolifyProxyUsed
}: CreateDockerDestination): Promise<string> {
const host = getEngine(engine);
const docker = dockerInstance({ destinationDocker: { engine, network } });
const found = await docker.engine.listNetworks({ filters: { name: [`^${network}$`] } });
@@ -99,18 +121,14 @@ export async function newLocalDestination({ name, teamId, engine, network, isCoo
(destination) => destination.network !== network && destination.isCoolifyProxyUsed === true
);
if (proxyConfigured) {
if (proxyConfigured.isCoolifyProxyUsed) {
isCoolifyProxyUsed = true;
} else {
isCoolifyProxyUsed = false;
}
isCoolifyProxyUsed = !!proxyConfigured.isCoolifyProxyUsed;
}
await prisma.destinationDocker.updateMany({ where: { engine }, data: { isCoolifyProxyUsed } });
}
if (isCoolifyProxyUsed) await startCoolifyProxy(engine);
return destination.id;
}
export async function removeDestination({ id }) {
export async function removeDestination({ id }: Pick<DestinationDocker, 'id'>): Promise<void> {
const destination = await prisma.destinationDocker.delete({ where: { id } });
if (destination.isCoolifyProxyUsed) {
const host = getEngine(destination.engine);
@@ -127,8 +145,11 @@ export async function removeDestination({ id }) {
}
}
export async function getDestination({ id, teamId }) {
let destination = {};
export async function getDestination({
id,
teamId
}: FindDestinationFromTeam): Promise<DestinationDocker & { sshPrivateKey?: string }> {
let destination;
if (teamId === '0') {
destination = await prisma.destinationDocker.findFirst({
where: { id }
@@ -141,13 +162,22 @@ export async function getDestination({ id, teamId }) {
return destination;
}
export async function getDestinationByApplicationId({ id, teamId }) {
export async function getDestinationByApplicationId({
id,
teamId
}: FindDestinationFromTeam): Promise<DestinationDocker> {
return await prisma.destinationDocker.findFirst({
where: { application: { some: { id } }, teams: { some: { id: teamId } } }
});
}
export async function setDestinationSettings({ engine, isCoolifyProxyUsed }) {
export async function setDestinationSettings({
engine,
isCoolifyProxyUsed
}: {
engine: string;
isCoolifyProxyUsed: boolean;
}): Promise<Prisma.BatchPayload> {
return await prisma.destinationDocker.updateMany({
where: { engine },
data: { isCoolifyProxyUsed }

View File

@@ -1,7 +1,10 @@
import { decrypt, encrypt } from '$lib/crypto';
import { prisma } from './common';
import type { GithubApp, GitlabApp, GitSource, Prisma, Application } from '@prisma/client';
export async function listSources(teamId) {
export async function listSources(
teamId: string | Prisma.StringFilter
): Promise<(GitSource & { githubApp?: GithubApp; gitlabApp?: GitlabApp })[]> {
if (teamId === '0') {
return await prisma.gitSource.findMany({
include: { githubApp: true, gitlabApp: true, teams: true }
@@ -13,7 +16,13 @@ export async function listSources(teamId) {
});
}
export async function newSource({ teamId, name }) {
export async function newSource({
name,
teamId
}: {
name: string;
teamId: string;
}): Promise<GitSource> {
return await prisma.gitSource.create({
data: {
name,
@@ -21,7 +30,7 @@ export async function newSource({ teamId, name }) {
}
});
}
export async function removeSource({ id }) {
export async function removeSource({ id }: { id: string }): Promise<void> {
const source = await prisma.gitSource.delete({
where: { id },
include: { githubApp: true, gitlabApp: true }
@@ -30,8 +39,14 @@ export async function removeSource({ id }) {
if (source.gitlabAppId) await prisma.gitlabApp.delete({ where: { id: source.gitlabAppId } });
}
export async function getSource({ id, teamId }) {
let body = {};
export async function getSource({
id,
teamId
}: {
id: string;
teamId: string;
}): Promise<GitSource & { githubApp: GithubApp; gitlabApp: GitlabApp }> {
let body;
if (teamId === '0') {
body = await prisma.gitSource.findFirst({
where: { id },
@@ -51,8 +66,11 @@ export async function getSource({ id, teamId }) {
if (body?.gitlabApp?.appSecret) body.gitlabApp.appSecret = decrypt(body.gitlabApp.appSecret);
return body;
}
export async function addGitHubSource({ id, teamId, type, name, htmlUrl, apiUrl }) {
await prisma.gitSource.update({ where: { id }, data: { type, name, htmlUrl, apiUrl } });
export async function addGitHubSource({ id, teamId, type, name, htmlUrl, apiUrl, organization }) {
await prisma.gitSource.update({
where: { id },
data: { type, name, htmlUrl, apiUrl, organization }
});
return await prisma.githubApp.create({
data: {
teams: { connect: { id: teamId } },
@@ -72,7 +90,7 @@ export async function addGitLabSource({
appSecret,
groupName
}) {
const encrptedAppSecret = encrypt(appSecret);
const encryptedAppSecret = encrypt(appSecret);
await prisma.gitSource.update({ where: { id }, data: { type, apiUrl, htmlUrl, name } });
return await prisma.gitlabApp.create({
data: {
@@ -80,19 +98,35 @@ export async function addGitLabSource({
appId,
oauthId,
groupName,
appSecret: encrptedAppSecret,
appSecret: encryptedAppSecret,
gitSource: { connect: { id } }
}
});
}
export async function configureGitsource({ id, gitSourceId }) {
export async function configureGitsource({
id,
gitSourceId
}: {
id: string;
gitSourceId: string;
}): Promise<Application> {
return await prisma.application.update({
where: { id },
data: { gitSource: { connect: { id: gitSourceId } } }
});
}
export async function updateGitsource({ id, name, htmlUrl, apiUrl }) {
export async function updateGitsource({
id,
name,
htmlUrl,
apiUrl
}: {
id: string;
name: string;
htmlUrl: string;
apiUrl: string;
}): Promise<GitSource> {
return await prisma.gitSource.update({
where: { id },
data: { name, htmlUrl, apiUrl }

View File

@@ -1,7 +1,15 @@
import { decrypt, encrypt } from '$lib/crypto';
import { prisma } from './common';
import type { GithubApp } from '@prisma/client';
export async function addInstallation({ gitSourceId, installation_id }) {
// TODO: We should change installation_id to be camelCase
export async function addInstallation({
gitSourceId,
installation_id
}: {
gitSourceId: string;
installation_id: string;
}): Promise<GithubApp> {
const source = await prisma.gitSource.findUnique({
where: { id: gitSourceId },
include: { githubApp: true }
@@ -12,8 +20,12 @@ export async function addInstallation({ gitSourceId, installation_id }) {
});
}
export async function getUniqueGithubApp({ githubAppId }) {
let body = await prisma.githubApp.findUnique({ where: { id: githubAppId } });
export async function getUniqueGithubApp({
githubAppId
}: {
githubAppId: string;
}): Promise<GithubApp> {
const body = await prisma.githubApp.findUnique({ where: { id: githubAppId } });
if (body.privateKey) body.privateKey = decrypt(body.privateKey);
return body;
}
@@ -26,7 +38,15 @@ export async function createGithubApp({
pem,
webhook_secret,
state
}) {
}: {
id: number;
client_id: string;
slug: string;
client_secret: string;
pem: string;
webhook_secret: string;
state: string;
}): Promise<GithubApp> {
const encryptedClientSecret = encrypt(client_secret);
const encryptedWebhookSecret = encrypt(webhook_secret);
const encryptedPem = encrypt(pem);

View File

@@ -1,7 +1,14 @@
import { encrypt } from '$lib/crypto';
import { generateSshKeyPair, prisma } from './common';
import type { GitlabApp } from '@prisma/client';
export async function updateDeployKey({ id, deployKeyId }) {
export async function updateDeployKey({
id,
deployKeyId
}: {
id: string;
deployKeyId: number;
}): Promise<GitlabApp> {
const application = await prisma.application.findUnique({
where: { id },
include: { gitSource: { include: { gitlabApp: true } } }
@@ -11,14 +18,24 @@ export async function updateDeployKey({ id, deployKeyId }) {
data: { deployKeyId }
});
}
export async function getSshKey({ id }) {
export async function getSshKey({
id
}: {
id: string;
}): Promise<{ status: number; body: { publicKey: string } }> {
const application = await prisma.application.findUnique({
where: { id },
include: { gitSource: { include: { gitlabApp: true } } }
});
return { status: 200, body: { publicKey: application.gitSource.gitlabApp.publicSshKey } };
}
export async function generateSshKey({ id }) {
export async function generateSshKey({
id
}: {
id: string;
}): Promise<
{ status: number; body: { publicKey: string } } | { status: number; body?: undefined }
> {
const application = await prisma.application.findUnique({
where: { id },
include: { gitSource: { include: { gitlabApp: true } } }

View File

@@ -1,6 +1,13 @@
import type { BuildLog } from '@prisma/client';
import { prisma, ErrorHandler } from './common';
export async function listLogs({ buildId, last = 0 }) {
export async function listLogs({
buildId,
last = 0
}: {
buildId: string;
last: number;
}): Promise<BuildLog[] | { status: number; body: { message: string; error: string } }> {
try {
const body = await prisma.buildLog.findMany({
where: { buildId, time: { gt: last } },

View File

@@ -1,7 +1,8 @@
import { encrypt, decrypt } from '$lib/crypto';
import { prisma } from './common';
import type { ServiceSecret, Secret, Prisma } from '@prisma/client';
export async function listServiceSecrets(serviceId: string) {
export async function listServiceSecrets(serviceId: string): Promise<ServiceSecret[]> {
let secrets = await prisma.serviceSecret.findMany({
where: { serviceId },
orderBy: { createdAt: 'desc' }
@@ -14,7 +15,7 @@ export async function listServiceSecrets(serviceId: string) {
return secrets;
}
export async function listSecrets(applicationId: string) {
export async function listSecrets(applicationId: string): Promise<Secret[]> {
let secrets = await prisma.secret.findMany({
where: { applicationId },
orderBy: { createdAt: 'desc' }
@@ -27,20 +28,48 @@ export async function listSecrets(applicationId: string) {
return secrets;
}
export async function createServiceSecret({ id, name, value }) {
export async function createServiceSecret({
id,
name,
value
}: {
id: string;
name: string;
value: string;
}): Promise<ServiceSecret> {
value = encrypt(value);
return await prisma.serviceSecret.create({
data: { name, value, service: { connect: { id } } }
});
}
export async function createSecret({ id, name, value, isBuildSecret, isPRMRSecret }) {
export async function createSecret({
id,
name,
value,
isBuildSecret,
isPRMRSecret
}: {
id: string;
name: string;
value: string;
isBuildSecret: boolean;
isPRMRSecret: boolean;
}): Promise<Secret> {
value = encrypt(value);
return await prisma.secret.create({
data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } }
});
}
export async function updateServiceSecret({ id, name, value }) {
export async function updateServiceSecret({
id,
name,
value
}: {
id: string;
name: string;
value: string;
}): Promise<Prisma.BatchPayload | ServiceSecret> {
value = encrypt(value);
const found = await prisma.serviceSecret.findFirst({ where: { serviceId: id, name } });
@@ -55,7 +84,19 @@ export async function updateServiceSecret({ id, name, value }) {
});
}
}
export async function updateSecret({ id, name, value, isBuildSecret, isPRMRSecret }) {
export async function updateSecret({
id,
name,
value,
isBuildSecret,
isPRMRSecret
}: {
id: string;
name: string;
value: string;
isBuildSecret: boolean;
isPRMRSecret: boolean;
}): Promise<Prisma.BatchPayload | Secret> {
value = encrypt(value);
const found = await prisma.secret.findFirst({ where: { applicationId: id, name, isPRMRSecret } });
@@ -71,10 +112,22 @@ export async function updateSecret({ id, name, value, isBuildSecret, isPRMRSecre
}
}
export async function removeServiceSecret({ id, name }) {
export async function removeServiceSecret({
id,
name
}: {
id: string;
name: string;
}): Promise<Prisma.BatchPayload> {
return await prisma.serviceSecret.deleteMany({ where: { serviceId: id, name } });
}
export async function removeSecret({ id, name }) {
export async function removeSecret({
id,
name
}: {
id: string;
name: string;
}): Promise<Prisma.BatchPayload> {
return await prisma.secret.deleteMany({ where: { applicationId: id, name } });
}

View File

@@ -1,10 +1,28 @@
import { asyncExecShell, getEngine } from '$lib/common';
import { decrypt, encrypt } from '$lib/crypto';
import type { Minio, Prisma, Service } from '@prisma/client';
import cuid from 'cuid';
import { generatePassword } from '.';
import { prisma } from './common';
export async function listServices(teamId) {
const include: Prisma.ServiceInclude = {
destinationDocker: true,
persistentStorage: true,
serviceSecret: true,
minio: true,
plausibleAnalytics: true,
vscodeserver: true,
wordpress: true,
ghost: true,
meiliSearch: true,
umami: true
};
export async function listServicesWithIncludes() {
return await prisma.service.findMany({
include,
orderBy: { createdAt: 'desc' }
});
}
export async function listServices(teamId: string): Promise<Service[]> {
if (teamId === '0') {
return await prisma.service.findMany({ include: { teams: true } });
} else {
@@ -15,22 +33,18 @@ export async function listServices(teamId) {
}
}
export async function newService({ name, teamId }) {
export async function newService({
name,
teamId
}: {
name: string;
teamId: string;
}): Promise<Service> {
return await prisma.service.create({ data: { name, teams: { connect: { id: teamId } } } });
}
export async function getService({ id, teamId }) {
let body = {};
const include = {
destinationDocker: true,
plausibleAnalytics: true,
minio: true,
vscodeserver: true,
wordpress: true,
ghost: true,
serviceSecret: true,
meiliSearch: true
};
export async function getService({ id, teamId }: { id: string; teamId: string }): Promise<Service> {
let body;
if (teamId === '0') {
body = await prisma.service.findFirst({
where: { id },
@@ -43,6 +57,12 @@ export async function getService({ id, teamId }) {
});
}
if (body?.serviceSecret.length > 0) {
body.serviceSecret = body.serviceSecret.map((s) => {
s.value = decrypt(s.value);
return s;
});
}
if (body.plausibleAnalytics?.postgresqlPassword)
body.plausibleAnalytics.postgresqlPassword = decrypt(
body.plausibleAnalytics.postgresqlPassword
@@ -69,21 +89,26 @@ export async function getService({ id, teamId }) {
if (body.meiliSearch?.masterKey) body.meiliSearch.masterKey = decrypt(body.meiliSearch.masterKey);
if (body?.serviceSecret.length > 0) {
body.serviceSecret = body.serviceSecret.map((s) => {
s.value = decrypt(s.value);
return s;
});
}
if (body.wordpress?.ftpPassword) {
body.wordpress.ftpPassword = decrypt(body.wordpress.ftpPassword);
}
if (body.wordpress?.ftpPassword) body.wordpress.ftpPassword = decrypt(body.wordpress.ftpPassword);
if (body.umami?.postgresqlPassword)
body.umami.postgresqlPassword = decrypt(body.umami.postgresqlPassword);
if (body.umami?.umamiAdminPassword)
body.umami.umamiAdminPassword = decrypt(body.umami.umamiAdminPassword);
if (body.umami?.hashSalt) body.umami.hashSalt = decrypt(body.umami.hashSalt);
const settings = await prisma.setting.findFirst();
return { ...body, settings };
}
export async function configureServiceType({ id, type }) {
export async function configureServiceType({
id,
type
}: {
id: string;
type: string;
}): Promise<void> {
if (type === 'plausibleanalytics') {
const password = encrypt(generatePassword());
const postgresqlUser = cuid();
@@ -197,48 +222,184 @@ export async function configureServiceType({ id, type }) {
meiliSearch: { create: { masterKey } }
}
});
} else if (type === 'umami') {
const umamiAdminPassword = encrypt(generatePassword());
const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword());
const postgresqlDatabase = 'umami';
const hashSalt = encrypt(generatePassword(64));
await prisma.service.update({
where: { id },
data: {
type,
umami: {
create: {
umamiAdminPassword,
postgresqlDatabase,
postgresqlPassword,
postgresqlUser,
hashSalt
}
}
}
});
}
}
export async function setServiceVersion({ id, version }) {
export async function setServiceVersion({
id,
version
}: {
id: string;
version: string;
}): Promise<Service> {
return await prisma.service.update({
where: { id },
data: { version }
});
}
export async function setServiceSettings({ id, dualCerts }) {
export async function setServiceSettings({
id,
dualCerts
}: {
id: string;
dualCerts: boolean;
}): Promise<Service> {
return await prisma.service.update({
where: { id },
data: { dualCerts }
});
}
export async function updatePlausibleAnalyticsService({ id, fqdn, email, username, name }) {
export async function updatePlausibleAnalyticsService({
id,
fqdn,
email,
username,
name
}: {
id: string;
fqdn: string;
name: string;
email: string;
username: string;
}): Promise<void> {
await prisma.plausibleAnalytics.update({ where: { serviceId: id }, data: { email, username } });
await prisma.service.update({ where: { id }, data: { name, fqdn } });
}
export async function updateService({ id, fqdn, name }) {
export async function updateService({
id,
fqdn,
name
}: {
id: string;
fqdn: string;
name: string;
}): Promise<Service> {
return await prisma.service.update({ where: { id }, data: { fqdn, name } });
}
export async function updateWordpress({ id, fqdn, name, mysqlDatabase, extraConfig }) {
export async function updateLanguageToolService({
id,
fqdn,
name
}: {
id: string;
fqdn: string;
name: string;
}): Promise<Service> {
return await prisma.service.update({ where: { id }, data: { fqdn, name } });
}
export async function updateMeiliSearchService({
id,
fqdn,
name
}: {
id: string;
fqdn: string;
name: string;
}): Promise<Service> {
return await prisma.service.update({ where: { id }, data: { fqdn, name } });
}
export async function updateVaultWardenService({
id,
fqdn,
name
}: {
id: string;
fqdn: string;
name: string;
}): Promise<Service> {
return await prisma.service.update({ where: { id }, data: { fqdn, name } });
}
export async function updateVsCodeServer({
id,
fqdn,
name
}: {
id: string;
fqdn: string;
name: string;
}): Promise<Service> {
return await prisma.service.update({ where: { id }, data: { fqdn, name } });
}
export async function updateWordpress({
id,
fqdn,
name,
mysqlDatabase,
extraConfig
}: {
id: string;
fqdn: string;
name: string;
mysqlDatabase: string;
extraConfig: string;
}): Promise<Service> {
return await prisma.service.update({
where: { id },
data: { fqdn, name, wordpress: { update: { mysqlDatabase, extraConfig } } }
});
}
export async function updateMinioService({ id, publicPort }) {
export async function updateMinioService({
id,
publicPort
}: {
id: string;
publicPort: number;
}): Promise<Minio> {
return await prisma.minio.update({ where: { serviceId: id }, data: { publicPort } });
}
export async function updateGhostService({ id, fqdn, name, mariadbDatabase }) {
export async function updateGhostService({
id,
fqdn,
name,
mariadbDatabase
}: {
id: string;
fqdn: string;
name: string;
mariadbDatabase: string;
}): Promise<Service> {
return await prisma.service.update({
where: { id },
data: { fqdn, name, ghost: { update: { mariadbDatabase } } }
});
}
export async function removeService({ id }) {
export async function removeService({ id }: { id: string }): Promise<void> {
await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id } });
await prisma.meiliSearch.deleteMany({ where: { serviceId: id } });
await prisma.ghost.deleteMany({ where: { serviceId: id } });
await prisma.umami.deleteMany({ where: { serviceId: id } });
await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } });
await prisma.minio.deleteMany({ where: { serviceId: id } });
await prisma.vscodeserver.deleteMany({ where: { serviceId: id } });

View File

@@ -1,8 +1,9 @@
import { decrypt } from '$lib/crypto';
import { prisma } from './common';
import type { Setting } from '@prisma/client';
export async function listSettings() {
let settings = await prisma.setting.findFirst({});
export async function listSettings(): Promise<Setting> {
const settings = await prisma.setting.findFirst({});
if (settings.proxyPassword) settings.proxyPassword = decrypt(settings.proxyPassword);
return settings;
}

View File

@@ -1,9 +1,10 @@
import type { Team, Permission } from '@prisma/client';
import { prisma } from './common';
export async function listTeams() {
export async function listTeams(): Promise<Team[]> {
return await prisma.team.findMany();
}
export async function newTeam({ name, userId }) {
export async function newTeam({ name, userId }: { name: string; userId: string }): Promise<Team> {
return await prisma.team.create({
data: {
name,
@@ -12,7 +13,11 @@ export async function newTeam({ name, userId }) {
}
});
}
export async function getMyTeams({ userId }) {
export async function getMyTeams({
userId
}: {
userId: string;
}): Promise<(Permission & { team: Team & { _count: { users: number } } })[]> {
return await prisma.permission.findMany({
where: { userId },
include: { team: { include: { _count: { select: { users: true } } } } }

View File

@@ -1,16 +1,30 @@
import cuid from 'cuid';
import bcrypt from 'bcrypt';
import bcrypt from 'bcryptjs';
import { prisma } from './common';
import { asyncExecShell, uniqueName } from '$lib/common';
import * as db from '$lib/database';
import { startCoolifyProxy } from '$lib/haproxy';
export async function hashPassword(password: string) {
import type { User } from '@prisma/client';
export async function hashPassword(password: string): Promise<string> {
const saltRounds = 15;
return bcrypt.hash(password, saltRounds);
}
export async function login({ email, password, isLogin }) {
export async function login({
email,
password,
isLogin
}: {
email: string;
password: string;
isLogin: boolean;
}): Promise<{
status: number;
headers: { 'Set-Cookie': string };
body: { userId: string; teamId: string; permission: string; isAdmin: boolean };
}> {
const users = await prisma.user.count();
const userFound = await prisma.user.findUnique({
where: { email },
@@ -32,8 +46,12 @@ export async function login({ email, password, isLogin }) {
if (users === 0) {
await prisma.setting.update({ where: { id }, data: { isRegistrationEnabled: false } });
// Create default network & start Coolify Proxy
await asyncExecShell(`docker network create --attachable coolify`);
await startCoolifyProxy('/var/run/docker.sock');
try {
await asyncExecShell(`docker network create --attachable coolify`);
} catch (error) {}
try {
await startCoolifyProxy('/var/run/docker.sock');
} catch (error) {}
uid = '0';
}
@@ -140,6 +158,6 @@ export async function login({ email, password, isLogin }) {
};
}
export async function getUser({ userId }) {
export async function getUser({ userId }: { userId: string }): Promise<User> {
return await prisma.user.findUnique({ where: { id: userId } });
}

View File

@@ -85,7 +85,8 @@ export async function buildImage({
docker,
buildId,
isCache = false,
debug = false
debug = false,
dockerFileLocation = '/Dockerfile'
}) {
if (isCache) {
await saveBuildLog({ line: `Building cache image started.`, buildId, applicationId });
@@ -103,11 +104,12 @@ export async function buildImage({
const stream = await docker.engine.buildImage(
{ src: ['.'], context: workdir },
{
dockerfile: isCache ? 'Dockerfile-cache' : 'Dockerfile',
dockerfile: isCache ? `${dockerFileLocation}-cache` : dockerFileLocation,
t: `${applicationId}:${tag}${isCache ? '-cache' : ''}`
}
);
await streamEvents({ stream, docker, buildId, applicationId, debug });
await saveBuildLog({ line: `Building image successful!`, buildId, applicationId });
}
export function dockerInstance({ destinationDocker }): { engine: Dockerode; network: string } {

View File

@@ -1,5 +1,6 @@
import { toast } from '@zerodevx/svelte-toast';
export function errorNotification(message: string) {
export function errorNotification(message: string): void {
console.error(message);
if (typeof message !== 'string') {
toast.push('Ooops, something is not okay, are you okay?');
@@ -30,7 +31,7 @@ export function enhance(
e.preventDefault();
let body = new FormData(form);
let parsedData = body;
const parsedData = body;
body.forEach((data, key) => {
if (data === '' || data === null) parsedData.delete(key);

View File

@@ -1,16 +1,16 @@
import { dev } from '$app/env';
import got from 'got';
import got, { type Got } from 'got';
import * as db from '$lib/database';
import mustache from 'mustache';
import crypto from 'crypto';
import * as db from '$lib/database';
import { checkContainer, checkHAProxy } from '.';
import { asyncExecShell, getDomain, getEngine } from '$lib/common';
import { supportedServiceTypesAndVersions } from '$lib/components/common';
import { listServicesWithIncludes } from '$lib/database';
const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555';
let template = `program api
const template = `program api
command /usr/bin/dataplaneapi -f /usr/local/etc/haproxy/dataplaneapi.hcl --userlist haproxy-dataplaneapi
no option start-on-reload
@@ -21,10 +21,10 @@ global
defaults
mode http
log global
timeout http-request 60s
timeout http-request 120s
timeout connect 10s
timeout client 60s
timeout server 60s
timeout client 120s
timeout server 120s
userlist haproxy-dataplaneapi
user admin insecure-password "\${HAPROXY_PASSWORD}"
@@ -128,7 +128,8 @@ backend {{domain}}
server {{id}} {{id}}:{{port}} check fall 10
{{/coolify}}
`;
export async function haproxyInstance() {
export async function haproxyInstance(): Promise<Got> {
const { proxyPassword } = await db.listSettings();
return got.extend({
prefixUrl: url,
@@ -137,31 +138,87 @@ export async function haproxyInstance() {
});
}
export async function configureHAProxy() {
export async function configureHAProxy(): Promise<void> {
const haproxy = await haproxyInstance();
await checkHAProxy(haproxy);
try {
const data = {
applications: [],
services: [],
coolify: []
};
const applications = await db.prisma.application.findMany({
include: { destinationDocker: true, settings: true }
});
for (const application of applications) {
const {
fqdn,
id,
port,
destinationDocker,
destinationDockerId,
settings: { previews },
updatedAt
} = application;
if (destinationDockerId) {
const { engine, network } = destinationDocker;
const data = {
applications: [],
services: [],
coolify: []
};
const applications = await db.prisma.application.findMany({
include: { destinationDocker: true, settings: true }
});
for (const application of applications) {
const {
fqdn,
id,
port,
destinationDocker,
destinationDockerId,
settings: { previews },
updatedAt
} = application;
if (destinationDockerId) {
const { engine, network } = destinationDocker;
const isRunning = await checkContainer(engine, id);
if (fqdn) {
const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
if (isRunning) {
data.applications.push({
id,
port: port || 3000,
domain,
isRunning,
isHttps,
redirectValue,
redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain,
updatedAt: updatedAt.getTime()
});
}
if (previews) {
const host = getEngine(engine);
const { stdout } = await asyncExecShell(
`DOCKER_HOST=${host} docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"`
);
const containers = stdout
.trim()
.split('\n')
.filter((a) => a)
.map((c) => c.replace(/"/g, ''));
if (containers.length > 0) {
for (const container of containers) {
const previewDomain = `${container.split('-')[1]}.${domain}`;
data.applications.push({
id: container,
port: port || 3000,
domain: previewDomain,
isRunning,
isHttps,
redirectValue,
redirectTo: isWWW ? previewDomain.replace('www.', '') : 'www.' + previewDomain,
updatedAt: updatedAt.getTime()
});
}
}
}
}
}
}
const services = await listServicesWithIncludes();
for (const service of services) {
const { fqdn, id, type, destinationDocker, destinationDockerId, updatedAt } = service;
if (destinationDockerId) {
const { engine } = destinationDocker;
const found = supportedServiceTypesAndVersions.find((a) => a.name === type);
if (found) {
const port = found.ports.main;
const publicPort = service[type]?.publicPort;
const isRunning = await checkContainer(engine, id);
if (fqdn) {
const domain = getDomain(fqdn);
@@ -169,9 +226,10 @@ export async function configureHAProxy() {
const isWWW = fqdn.includes('www.');
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
if (isRunning) {
data.applications.push({
data.services.push({
id,
port: port || 3000,
port,
publicPort,
domain,
isRunning,
isHttps,
@@ -180,108 +238,38 @@ export async function configureHAProxy() {
updatedAt: updatedAt.getTime()
});
}
if (previews) {
const host = getEngine(engine);
const { stdout } = await asyncExecShell(
`DOCKER_HOST=${host} docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"`
);
const containers = stdout
.trim()
.split('\n')
.filter((a) => a)
.map((c) => c.replace(/"/g, ''));
if (containers.length > 0) {
for (const container of containers) {
let previewDomain = `${container.split('-')[1]}.${domain}`;
data.applications.push({
id: container,
port: port || 3000,
domain: previewDomain,
isRunning,
isHttps,
redirectValue,
redirectTo: isWWW ? previewDomain.replace('www.', '') : 'www.' + previewDomain,
updatedAt: updatedAt.getTime()
});
}
}
}
}
}
}
const services = await db.prisma.service.findMany({
include: {
destinationDocker: true,
minio: true,
plausibleAnalytics: true,
vscodeserver: true,
wordpress: true,
ghost: true
}
const { fqdn } = await db.prisma.setting.findFirst();
if (fqdn) {
const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
data.coolify.push({
id: dev ? 'host.docker.internal' : 'coolify',
port: 3000,
domain,
isHttps,
redirectValue,
redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain
});
}
const output = mustache.render(template, data);
const newHash = crypto.createHash('md5').update(output).digest('hex');
const { proxyHash, id } = await db.listSettings();
if (proxyHash !== newHash) {
await db.prisma.setting.update({ where: { id }, data: { proxyHash: newHash } });
await haproxy.post(`v2/services/haproxy/configuration/raw`, {
searchParams: {
skip_version: true
},
body: output,
headers: {
'Content-Type': 'text/plain'
}
});
for (const service of services) {
const { fqdn, id, type, destinationDocker, destinationDockerId, updatedAt } = service;
if (destinationDockerId) {
const { engine } = destinationDocker;
const found = supportedServiceTypesAndVersions.find((a) => a.name === type);
if (found) {
const port = found.ports.main;
const publicPort = service[type]?.publicPort;
const isRunning = await checkContainer(engine, id);
if (fqdn) {
const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
if (isRunning) {
data.services.push({
id,
port,
publicPort,
domain,
isRunning,
isHttps,
redirectValue,
redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain,
updatedAt: updatedAt.getTime()
});
}
}
}
}
}
const { fqdn } = await db.prisma.setting.findFirst();
if (fqdn) {
const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
data.coolify.push({
id: dev ? 'host.docker.internal' : 'coolify',
port: 3000,
domain,
isHttps,
redirectValue,
redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain
});
}
const output = mustache.render(template, data);
const newHash = crypto.createHash('md5').update(output).digest('hex');
const { proxyHash, id } = await db.listSettings();
if (proxyHash !== newHash) {
await db.prisma.setting.update({ where: { id }, data: { proxyHash: newHash } });
await haproxy.post(`v2/services/haproxy/configuration/raw`, {
searchParams: {
skip_version: true
},
body: output,
headers: {
'Content-Type': 'text/plain'
}
});
}
} catch (error) {
throw error;
}
}

View File

@@ -1,7 +1,8 @@
import { dev } from '$app/env';
import { asyncExecShell, getEngine } from '$lib/common';
import got from 'got';
import got, { type Got, type Response } from 'got';
import * as db from '$lib/database';
import type { DestinationDocker } from '@prisma/client';
const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555';
@@ -9,7 +10,7 @@ export const defaultProxyImage = `coolify-haproxy-alpine:latest`;
export const defaultProxyImageTcp = `coolify-haproxy-tcp-alpine:latest`;
export const defaultProxyImageHttp = `coolify-haproxy-http-alpine:latest`;
export async function haproxyInstance() {
export async function haproxyInstance(): Promise<Got> {
const { proxyPassword } = await db.listSettings();
return got.extend({
prefixUrl: url,
@@ -17,6 +18,7 @@ export async function haproxyInstance() {
password: proxyPassword
});
}
export async function getRawConfiguration(): Promise<RawHaproxyConfiguration> {
return await (await haproxyInstance()).get(`v2/services/haproxy/configuration/raw`).json();
}
@@ -43,11 +45,12 @@ export async function getNextTransactionId(): Promise<string> {
return newTransaction.id;
}
export async function completeTransaction(transactionId) {
export async function completeTransaction(transactionId: string): Promise<Response<string>> {
const haproxy = await haproxyInstance();
return await haproxy.put(`v2/services/haproxy/transactions/${transactionId}`);
}
export async function deleteProxy({ id }) {
export async function deleteProxy({ id }: { id: string }): Promise<void> {
const haproxy = await haproxyInstance();
await checkHAProxy(haproxy);
@@ -77,11 +80,12 @@ export async function deleteProxy({ id }) {
}
}
export async function reloadHaproxy(engine) {
export async function reloadHaproxy(engine: string): Promise<{ stdout: string; stderr: string }> {
const host = getEngine(engine);
return await asyncExecShell(`DOCKER_HOST=${host} docker exec coolify-haproxy kill -HUP 1`);
}
export async function checkHAProxy(haproxy?: any) {
export async function checkHAProxy(haproxy?: Got): Promise<void> {
if (!haproxy) haproxy = await haproxyInstance();
try {
await haproxy.get('v2/info');
@@ -93,7 +97,10 @@ export async function checkHAProxy(haproxy?: any) {
}
}
export async function stopTcpHttpProxy(destinationDocker, publicPort) {
export async function stopTcpHttpProxy(
destinationDocker: DestinationDocker,
publicPort: number
): Promise<{ stdout: string; stderr: string } | Error> {
const { engine } = destinationDocker;
const host = getEngine(engine);
const containerName = `haproxy-for-${publicPort}`;
@@ -108,16 +115,22 @@ export async function stopTcpHttpProxy(destinationDocker, publicPort) {
return error;
}
}
export async function startTcpProxy(destinationDocker, id, publicPort, privatePort, volume = null) {
export async function startTcpProxy(
destinationDocker: DestinationDocker,
id: string,
publicPort: number,
privatePort: number,
volume?: string
): Promise<{ stdout: string; stderr: string } | Error> {
const { network, engine } = destinationDocker;
const host = getEngine(engine);
const containerName = `haproxy-for-${publicPort}`;
const found = await checkContainer(engine, containerName);
const foundDB = await checkContainer(engine, id);
const found = await checkContainer(engine, containerName, true);
const foundDependentContainer = await checkContainer(engine, id, true);
try {
if (foundDB && !found) {
if (foundDependentContainer && !found) {
const { stdout: Config } = await asyncExecShell(
`DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'`
);
@@ -128,20 +141,31 @@ export async function startTcpProxy(destinationDocker, id, publicPort, privatePo
} -d coollabsio/${defaultProxyImageTcp}`
);
}
if (!foundDependentContainer && found) {
return await asyncExecShell(
`DOCKER_HOST=${host} docker stop -t 0 ${containerName} && docker rm ${containerName}`
);
}
} catch (error) {
return error;
}
}
export async function startHttpProxy(destinationDocker, id, publicPort, privatePort) {
export async function startHttpProxy(
destinationDocker: DestinationDocker,
id: string,
publicPort: number,
privatePort: number
): Promise<{ stdout: string; stderr: string } | Error> {
const { network, engine } = destinationDocker;
const host = getEngine(engine);
const containerName = `haproxy-for-${publicPort}`;
const found = await checkContainer(engine, containerName);
const foundDB = await checkContainer(engine, id);
const found = await checkContainer(engine, containerName, true);
const foundDependentContainer = await checkContainer(engine, id, true);
try {
if (foundDB && !found) {
if (foundDependentContainer && !found) {
const { stdout: Config } = await asyncExecShell(
`DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'`
);
@@ -150,13 +174,19 @@ export async function startHttpProxy(destinationDocker, id, publicPort, privateP
`DOCKER_HOST=${host} docker run --restart always -e PORT=${publicPort} -e APP=${id} -e PRIVATE_PORT=${privatePort} --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' --network ${network} -p ${publicPort}:${publicPort} --name ${containerName} -d coollabsio/${defaultProxyImageHttp}`
);
}
if (!foundDependentContainer && found) {
return await asyncExecShell(
`DOCKER_HOST=${host} docker stop -t 0 ${containerName} && docker rm ${containerName}`
);
}
} catch (error) {
return error;
}
}
export async function startCoolifyProxy(engine) {
export async function startCoolifyProxy(engine: string): Promise<void> {
const host = getEngine(engine);
const found = await checkContainer(engine, 'coolify-haproxy');
const found = await checkContainer(engine, 'coolify-haproxy', true);
const { proxyPassword, proxyUser, id } = await db.listSettings();
if (!found) {
const { stdout: Config } = await asyncExecShell(
@@ -170,7 +200,26 @@ export async function startCoolifyProxy(engine) {
}
await configureNetworkCoolifyProxy(engine);
}
export async function checkContainer(engine, container) {
export async function isContainerExited(engine: string, containerName: string): Promise<boolean> {
let isExited = false;
const host = getEngine(engine);
try {
const { stdout } = await asyncExecShell(
`DOCKER_HOST="${host}" docker inspect -f '{{.State.Status}}' ${containerName}`
);
if (stdout.trim() === 'exited') {
isExited = true;
}
} catch (error) {}
return isExited;
}
export async function checkContainer(
engine: string,
container: string,
remove: boolean = false
): Promise<boolean> {
const host = getEngine(engine);
let containerFound = false;
@@ -180,8 +229,11 @@ export async function checkContainer(engine, container) {
);
const parsedStdout = JSON.parse(stdout);
const status = parsedStdout.Status;
const isRunning = status === 'running' ? true : false;
if (status === 'exited' || status === 'created') {
const isRunning = status === 'running';
if (status === 'created') {
await asyncExecShell(`DOCKER_HOST="${host}" docker rm ${container}`);
}
if (remove && status === 'exited') {
await asyncExecShell(`DOCKER_HOST="${host}" docker rm ${container}`);
}
if (isRunning) {
@@ -193,7 +245,9 @@ export async function checkContainer(engine, container) {
return containerFound;
}
export async function stopCoolifyProxy(engine) {
export async function stopCoolifyProxy(
engine: string
): Promise<{ stdout: string; stderr: string } | Error> {
const host = getEngine(engine);
const found = await checkContainer(engine, 'coolify-haproxy');
await db.setDestinationSettings({ engine, isCoolifyProxyUsed: false });
@@ -210,16 +264,18 @@ export async function stopCoolifyProxy(engine) {
}
}
export async function configureNetworkCoolifyProxy(engine) {
export async function configureNetworkCoolifyProxy(engine: string): Promise<void> {
const host = getEngine(engine);
const destinations = await db.prisma.destinationDocker.findMany({ where: { engine } });
destinations.forEach(async (destination) => {
try {
const { stdout: networks } = await asyncExecShell(
`DOCKER_HOST="${host}" docker ps -a --filter name=coolify-haproxy --format '{{json .Networks}}'`
);
const configuredNetworks = networks.replace(/"/g, '').replace('\n', '').split(',');
for (const destination of destinations) {
if (!configuredNetworks.includes(destination.network)) {
await asyncExecShell(
`DOCKER_HOST="${host}" docker network connect ${destination.network} coolify-haproxy`
);
} catch (err) {
// TODO: handle error
}
});
}
}

View File

@@ -2,11 +2,9 @@ import { asyncExecShell, saveBuildLog } from '$lib/common';
import got from 'got';
import jsonwebtoken from 'jsonwebtoken';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
export default async function ({
applicationId,
debug,
workdir,
githubAppId,
repository,
@@ -14,7 +12,16 @@ export default async function ({
htmlUrl,
branch,
buildId
}): Promise<any> {
}: {
applicationId: string;
workdir: string;
githubAppId: string;
repository: string;
apiUrl: string;
htmlUrl: string;
branch: string;
buildId: string;
}): Promise<string> {
const url = htmlUrl.replace('https://', '').replace('http://', '');
await saveBuildLog({ line: 'GitHub importer started.', buildId, applicationId });
const { privateKey, appId, installationId } = await db.getUniqueGithubApp({ githubAppId });

View File

@@ -9,7 +9,16 @@ export default async function ({
branch,
buildId,
privateSshKey
}): Promise<any> {
}: {
applicationId: string;
workdir: string;
repository: string;
htmlUrl: string;
branch: string;
buildId: string;
repodir: string;
privateSshKey: string;
}): Promise<string> {
const url = htmlUrl.replace('https://', '').replace('http://', '').replace(/\/$/, '');
await saveBuildLog({ line: 'GitLab importer started.', buildId, applicationId });
await asyncExecShell(`echo '${privateSshKey}' > ${repodir}/id.rsa`);

4
src/lib/lang.json Normal file
View File

@@ -0,0 +1,4 @@
{
"fr": "Français",
"en": "English"
}

View File

@@ -6,9 +6,14 @@ import cuid from 'cuid';
import fs from 'fs/promises';
import getPort, { portNumbers } from 'get-port';
import { supportedServiceTypesAndVersions } from '$lib/components/common';
import { promises as dns } from 'dns';
import { listServicesWithIncludes } from '$lib/database';
export async function letsEncrypt(domain, id = null, isCoolify = false) {
export async function letsEncrypt(domain: string, id?: string, isCoolify = false): Promise<void> {
try {
const certbotImage =
process.arch === 'x64' ? 'certbot/certbot' : 'certbot/certbot:arm64v8-latest';
const data = await db.prisma.setting.findFirst();
const { minPort, maxPort } = data;
@@ -62,7 +67,7 @@ export async function letsEncrypt(domain, id = null, isCoolify = false) {
if (found) return;
await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:${randomPort} -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port ${randomPort} -d ${nakedDomain} -d ${wwwDomain} --expand --agree-tos --non-interactive --register-unsafely-without-email ${
`DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:${randomPort} -v "coolify-letsencrypt:/etc/letsencrypt" ${certbotImage} --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port ${randomPort} -d ${nakedDomain} -d ${wwwDomain} --expand --agree-tos --non-interactive --register-unsafely-without-email ${
dev ? '--test-cert' : ''
}`
);
@@ -82,7 +87,7 @@ export async function letsEncrypt(domain, id = null, isCoolify = false) {
}
if (found) return;
await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:${randomPort} -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port ${randomPort} -d ${domain} --expand --agree-tos --non-interactive --register-unsafely-without-email ${
`DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:${randomPort} -v "coolify-letsencrypt:/etc/letsencrypt" ${certbotImage} --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port ${randomPort} -d ${domain} --expand --agree-tos --non-interactive --register-unsafely-without-email ${
dev ? '--test-cert' : ''
}`
);
@@ -98,7 +103,7 @@ export async function letsEncrypt(domain, id = null, isCoolify = false) {
}
}
export async function generateSSLCerts() {
export async function generateSSLCerts(): Promise<void> {
const ssls = [];
const applications = await db.prisma.application.findMany({
include: { destinationDocker: true, settings: true },
@@ -131,7 +136,7 @@ export async function generateSSLCerts() {
.map((c) => c.replace(/"/g, ''));
if (containers.length > 0) {
for (const container of containers) {
let previewDomain = `${container.split('-')[1]}.${domain}`;
const previewDomain = `${container.split('-')[1]}.${domain}`;
if (isHttps) ssls.push({ domain: previewDomain, id, isCoolify: false });
}
}
@@ -141,17 +146,7 @@ export async function generateSSLCerts() {
console.log(`Error during generateSSLCerts with ${application.fqdn}: ${error}`);
}
}
const services = await db.prisma.service.findMany({
include: {
destinationDocker: true,
minio: true,
plausibleAnalytics: true,
vscodeserver: true,
wordpress: true,
ghost: true
},
orderBy: { createdAt: 'desc' }
});
const services = await listServicesWithIncludes();
for (const service of services) {
try {
@@ -198,16 +193,44 @@ export async function generateSSLCerts() {
file.endsWith('.pem') && certificates.push(file.replace(/\.pem$/, ''));
}
}
const resolver = new dns.Resolver({ timeout: 2000 });
resolver.setServers(['8.8.8.8', '1.1.1.1']);
let ipv4, ipv6;
try {
ipv4 = await (await asyncExecShell(`curl -4s https://ifconfig.io`)).stdout;
} catch (error) {}
try {
ipv6 = await (await asyncExecShell(`curl -6s https://ifconfig.io`)).stdout;
} catch (error) {}
for (const ssl of ssls) {
if (!dev) {
if (
certificates.includes(ssl.domain) ||
certificates.includes(ssl.domain.replace('www.', ''))
) {
console.log(`Certificate for ${ssl.domain} already exists`);
// console.log(`Certificate for ${ssl.domain} already exists`);
} else {
console.log('Generating SSL for', ssl.domain);
await letsEncrypt(ssl.domain, ssl.id, ssl.isCoolify);
// Checking DNS entry before generating certificate
if (ipv4 || ipv6) {
let domains4 = [];
let domains6 = [];
try {
domains4 = await resolver.resolve4(ssl.domain);
} catch (error) {}
try {
domains6 = await resolver.resolve6(ssl.domain);
} catch (error) {}
if (domains4.length > 0 || domains6.length > 0) {
if (
(ipv4 && domains4.includes(ipv4.replace('\n', ''))) ||
(ipv6 && domains6.includes(ipv6.replace('\n', '')))
) {
console.log('Generating SSL for', ssl.domain);
return await letsEncrypt(ssl.domain, ssl.id, ssl.isCoolify);
}
}
}
console.log('DNS settings is incorrect for', ssl.domain, 'skipping.');
}
} else {
if (
@@ -216,7 +239,27 @@ export async function generateSSLCerts() {
) {
console.log(`Certificate for ${ssl.domain} already exists`);
} else {
console.log('Generating SSL for', ssl.domain);
// Checking DNS entry before generating certificate
if (ipv4 || ipv6) {
let domains4 = [];
let domains6 = [];
try {
domains4 = await resolver.resolve4(ssl.domain);
} catch (error) {}
try {
domains6 = await resolver.resolve6(ssl.domain);
} catch (error) {}
if (domains4.length > 0 || domains6.length > 0) {
if (
(ipv4 && domains4.includes(ipv4.replace('\n', ''))) ||
(ipv6 && domains6.includes(ipv6.replace('\n', '')))
) {
console.log('Generating SSL for', ssl.domain);
return;
}
}
}
console.log('DNS settings is incorrect for', ssl.domain, 'skipping.');
}
}
}

329
src/lib/locales/en.json Normal file
View File

@@ -0,0 +1,329 @@
{
"layout": {
"update_done": "Update completed.",
"wait_new_version_startup": "Waiting for the new version to start...",
"new_version": "New version reachable. Reloading...",
"switch_to_a_different_team": "Switch to a different team...",
"update_available": "Update available"
},
"error": {
"you_can_find_your_way_back": "You can find your way back",
"here": "here",
"you_are_lost": "Ooops you are lost! But don't be afraid!"
},
"index": {
"dashboard": "Dashboard",
"applications": "Applications",
"destinations": "Destinations",
"git_sources": "Git Sources",
"databases": "Databases",
"services": "Services",
"teams": "Teams",
"not_implemented_yet": "Not implemented yet",
"database": "Database",
"settings": "Settings",
"global_settings": "Global Settings",
"secret": "Secret",
"team": "Team",
"logout": "Logout"
},
"login": {
"already_logged_in": "Already logged in...",
"authenticating": "Authenticating...",
"login": "Login"
},
"forms": {
"password": "Password",
"email": "Email address",
"passwords_not_match": "Passwords do not match.",
"password_again": "Password again",
"save": "Save",
"saving": "Saving...",
"name": "Name",
"value": "Value",
"action": "Action",
"is_required": "is required.",
"add": "Add",
"set": "Set",
"remove": "Remove",
"path": "Path",
"confirm_continue": "Are you sure to continue?",
"must_be_stopped_to_modify": "Must be stopped to modify.",
"port": "Port",
"default": "default",
"base_directory": "Base Directory",
"publish_directory": "Publish Directory",
"generated_automatically_after_start": "Generated automatically after start",
"roots_password": "Root's Password",
"root_user": "Root User",
"eg": "eg",
"user": "User",
"loading": "Loading...",
"version": "Version",
"host": "Host",
"already_used_for": "<span class=\"text-red-500\">{{type}}</span> already used for",
"configuration": "Configuration",
"engine": "Engine",
"network": "Network",
"ip_address": "IP Address",
"ssh_private_key": "SSH Private Key",
"type": "Type",
"html_url": "HTML URL",
"api_url": "API URL",
"organization": "Organization",
"new_password": "New password",
"super_secure_new_password": "Super secure new password",
"submit": "Submit",
"default_email_address": "Default Email Address",
"default_password": "Default Password",
"username": "Username",
"root_db_user": "Root DB User",
"root_db_password": "Root DB Password",
"api_port": "API Port",
"verifying": "Verifying",
"verify_emails_without_smtp": "Verify emails without SMTP",
"extra_config": "Extra Config",
"select_a_service": "Select a Service",
"select_a_service_version": "Select a Service version",
"removing": "Removing...",
"remove_domain": "Remove domain",
"public_port_range": "Public Port Range",
"public_port_range_explainer": "Ports used to expose databases/services/internal services.<br> Add them to your firewall (if applicable).<br><br>You can specify a range of ports, eg: <span class='text-yellow-500 font-bold'>9000-9100</span>",
"no_actions_available": "No actions available",
"admin_api_key": "Admin API key"
},
"register": {
"register": "Register",
"registering": "Registering...",
"first_user": "You are registering the first user. It will be the administrator of your Coolify instance."
},
"reset": {
"reset_password": "Reset",
"invalid_secret_key": "Invalid secret key.",
"secret_key": "Secret Key",
"find_path_secret_key": "You can find it in ~/coolify/.env (COOLIFY_SECRET_KEY)"
},
"application": {
"configuration": {
"buildpack": {
"choose_this_one": "Choose this one..."
},
"branch_already_in_use": "This branch is already used by another application. Webhooks won't work in this case for both applications. Are you sure you want to use it?",
"no_repositories_configured": "No repositories configured for your Git Application.",
"configure_it_now": "Configure it now",
"loading_repositories": "Loading repositories ...",
"select_a_repository": "Please select a repository",
"loading_branches": "Loading branches ...",
"select_a_repository_first": "Please select a repository first",
"select_a_branch": "Please select a branch",
"loading_groups": "Loading groups...",
"select_a_group": "Please select a group",
"loading_projects": "Loading projects...",
"select_a_project": "Please select a project",
"no_projects_found": "No projects found",
"no_branches_found": "No branches found",
"configure_build_pack": "Configure Build Pack",
"scanning_repository_suggest_build_pack": "Scanning repository to suggest a build pack for you...",
"found_lock_file": "Found lock file for <span class=\"font-bold text-orange-500 pl-1\">{{packageManager}}</span>. Using it for predefined commands commands.",
"configure_destination": "Configure Destination",
"no_configurable_destination": "No configurable Destination found",
"select_a_repository_project": "Select a Repository / Project",
"select_a_git_source": "Select a Git Source",
"no_configurable_git": "No configurable Git Source found",
"configuration_missing": "Configuration missing"
},
"build": {
"queued_waiting_exec": "Queued and waiting for execution.",
"build_logs_of": "Build logs of",
"running": "Running",
"queued": "Queued",
"finished_in": "Finished in",
"load_more": "Load More",
"no_logs": "No logs found",
"waiting_logs": "Waiting for the logs..."
},
"preview": {
"need_during_buildtime": "Need during buildtime?",
"setup_secret_app_first": "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments.",
"values_overwriting_app_secrets": "These values overwrite application secrets in PR/MR deployments. Useful for creating <span class='text-green-500 font-bold'>staging</span> environments.",
"redeploy": "Redeploy",
"no_previews_available": "No previews available"
},
"secrets": {
"secret_saved": "Secret saved.",
"use_isbuildsecret": "Use isBuildSecret",
"secrets_for": "Secrets for"
},
"storage": {
"path_is_required": "Path is required.",
"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."
},
"deployment_queued": "Deployment queued.",
"confirm_to_delete": "Are you sure you would like to delete '{{name}}'?",
"stop_application": "Stop application",
"permission_denied_stop_application": "You do not have permission to stop the application.",
"rebuild_application": "Rebuild application",
"permission_denied_rebuild_application": "You do not have permission to rebuild application.",
"build_and_start_application": "Build and start application",
"permission_denied_build_and_start_application": "You do not have permission to Build and start application.",
"configurations": "Configurations",
"secret": "Secrets",
"persistent_storage": "Persistent Storage",
"previews": "Previews",
"logs": "Application Logs",
"build_logs": "Build Logs",
"delete_application": "Delete application",
"permission_denied_delete_application": "You do not have permission to delete this application",
"domain_already_in_use": "Domain {{domain}} is already used.",
"dns_not_set_error": "DNS not set or propogated for {{domain}}.<br><br>Please check your DNS settings.",
"settings_saved": "Settings saved.",
"dns_not_set_partial_error": "DNS not set",
"git_source": "Git Source",
"git_repository": "Git Repository",
"build_pack": "Build Pack",
"destination": "Destination",
"application": "Application",
"url_fqdn": "URL (FQDN)",
"domain_fqdn": "Domain (FQDN)",
"https_explainer": "If you specify <span class='text-green-500 font-bold'>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-green-500 font-bold'>www</span>, the application will be redirected (302) from non-www and vice versa.<br><br>To modify the domain, you must first stop the application.<br><br><span class='text-white font-bold'>You must set your DNS to point to the server IP in advance.</span>",
"ssl_www_and_non_www": "Generate SSL for www and non-www?",
"ssl_explainer": "It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-green-500'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both.",
"install_command": "Install Command",
"build_command": "Build Command",
"start_command": "Start Command",
"directory_to_use_explainer": "Directory to use as the base for all commands.<br>Could be useful with <span class='text-green-500 font-bold'>monorepos</span>.",
"publish_directory_explainer": "Directory containing all the assets for deployment. <br> For example: <span class='text-green-500 font-bold'>dist</span>,<span class='text-green-500 font-bold'>_site</span> or <span class='text-green-500 font-bold'>public</span>.",
"features": "Features",
"enable_automatic_deployment": "Enable Automatic Deployment",
"enable_auto_deploy_webhooks": "Enable automatic deployment through webhooks.",
"enable_mr_pr_previews": "Enable MR/PR Previews",
"enable_preview_deploy_mr_pr_requests": "Enable preview deployments from pull or merge requests.",
"debug_logs": "Debug Logs",
"enable_debug_log_during_build": "Enable debug logs during build phase.<br><span class='text-red-500 font-bold'>Sensitive information</span> could be visible and saved in logs.",
"cant_activate_auto_deploy_without_repo": "Cannot activate automatic deployments until only one application is defined for this repository / branch.",
"no_applications_found": "No applications found",
"secret__batch_dot_env": "Paste .env file",
"batch_secrets": "Batch add secrets"
},
"general": "General",
"database": {
"default_database": "Default Database",
"generated_automatically_after_set_to_public": "Generated automatically after set to public",
"connection_string": "Connection String",
"set_public": "Set it public",
"warning_database_public": "Your database will be reachable over the internet. <br>Take security seriously in this case!",
"change_append_only_mode": "Change append only mode",
"warning_append_only": "Useful if you would like to restore redis data from a backup.<br><span class='font-bold text-white'>Database restart is required.</span>",
"select_database_type": "Select a Database type",
"select_database_version": "Select a Database version",
"confirm_stop": "Are you sure you would like to stop {{name}}?",
"stop_database": "Stop database",
"permission_denied_stop_database": "You do not have permission to stop the database.",
"start_database": "Start database",
"permission_denied_start_database": "You do not have permission to start the database.",
"delete_database": "Delete Database",
"permission_denied_delete_database": "You do not have permission to delete a Database",
"no_databases_found": "No databases found"
},
"destination": {
"delete_destination": "Delete Destination",
"permission_denied_delete_destination": "You do not have permission to delete this destination",
"add_to_coolify": "Add to Coolify",
"coolify_proxy_stopped": "Coolify Proxy stopped!",
"coolify_proxy_started": "Coolify Proxy started!",
"confirm_restart_proxy": "Are you sure you want to restart the proxy? Everything will be reconfigured in ~10 secs.",
"coolify_proxy_restarting": "Coolify Proxy restarting...",
"restarting_please_wait": "Restarting... please wait...",
"force_restart_proxy": "Force restart proxy",
"use_coolify_proxy": "Use Coolify Proxy?",
"no_destination_found": "No destination found",
"new_error_network_already_exists": "Network {{network}} already configured for another team!",
"new": {
"saving_and_configuring_proxy": "Saving and configuring proxy...",
"install_proxy": "This will install a proxy on the destination to allow you to access your applications and services without any manual configuration (recommended for Docker).<br><br>Databases will have their own proxy.",
"add_new_destination": "Add New Destination",
"predefined_destinations": "Predefined destinations"
}
},
"sources": {
"local_docker": "Local Docker",
"remote_docker": "Remote Docker",
"organization_explainer": "Fill it if you would like to use an organization's as your Git Source. Otherwise your user will be used."
},
"source": {
"new": {
"git_source": "Add New Git Source",
"official_providers": "Official providers"
},
"no_git_sources_found": "No git sources found",
"delete_git_source": "Delete Git Source",
"permission_denied": "You do not have permission to delete a Git Source",
"create_new_app": "Create new {{name}} App",
"change_app_settings": "Change {{name}} App Settings",
"install_repositories": "Install Repositories",
"application_id": "Application ID",
"group_name": "Group Name",
"oauth_id": "OAuth ID",
"oauth_id_explainer": "The OAuth ID is the unique identifier of the GitLab application. <br>You can find it <span class='font-bold text-orange-600' >in the URL</span> of your GitLab OAuth Application.",
"register_oauth_gitlab": "Register new OAuth application on GitLab",
"gitlab": {
"self_hosted": "Instance-wide application (self-hosted)",
"user_owned": "User owned application",
"group_owned": "Group owned application",
"gitlab_application_type": "GitLab Application Type",
"already_configured": "GitLab App is already configured."
},
"github": {
"redirecting": "Redirecting to Github..."
}
},
"services": {
"all_email_verified": "All email verified. You can login now.",
"generate_www_non_www_ssl": "It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-pink-600'>both DNS entries</span> set in advance.<br><br>Service needs to be restarted."
},
"service": {
"stop_service": "Stop Service",
"permission_denied_stop_service": "You do not have permission to stop the service.",
"start_service": "Start Service",
"permission_denied_start_service": "You do not have permission to start the service.",
"delete_service": "Delete Service",
"permission_denied_delete_service": "You do not have permission to delete a service.",
"no_service": "No services found"
},
"setting": {
"change_language": "Change Language",
"permission_denied": "You do not have permission to do this. \\nAsk an admin to modify your permissions.",
"domain_removed": "Domain removed",
"ssl_explainer": "If you specify <span class='text-yellow-500 font-bold'>https</span>, Coolify will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-yellow-500 font-bold'>www</span>, Coolify will be redirected (302) from non-www and vice versa.",
"must_remove_domain_before_changing": "Must remove the domain before you can change this setting.",
"registration_allowed": "Registration allowed?",
"registration_allowed_explainer": "Allow further registrations to the application. <br>It's turned off after the first registration.",
"coolify_proxy_settings": "Coolify Proxy Settings",
"credential_stat_explainer": "Credentials for <a class=\"text-white font-bold\" href=\"{{link}}\" target=\"_blank\">stats</a> page.",
"auto_update_enabled": "Auto update enabled?",
"auto_update_enabled_explainer": "Enable automatic updates for Coolify. It will be done automatically behind the scenes, if there is no build process running."
},
"team": {
"pending_invitations": "Pending invitations",
"accept": "Accept",
"delete": "Delete",
"member": "member(s)",
"root": "(root)",
"invited_with_permissions": "Invited to <span class=\"font-bold text-pink-600\">{{teamName}}</span> with <span class=\"font-bold text-rose-600\">{{permission}}</span> permission.",
"members": "Members",
"root_team_explainer": "This is the <span class='text-red-500 font-bold'>root</span> team. That means members of this group can manage instance wide settings and have all the priviliges in Coolify (imagine like root user on Linux).",
"permission": "Permission",
"you": "(You)",
"promote_to": "Promote to {{grade}}",
"revoke_invitation": "Revoke invitation",
"pending_invitation": "Pending invitation",
"invite_new_member": "Invite new member",
"send_invitation": "Send invitation",
"invite_only_register_explainer": "You can only invite registered users at the moment - will be extended soon.",
"admin": "Admin",
"read": "Read"
}
}

322
src/lib/locales/fr.json Normal file
View File

@@ -0,0 +1,322 @@
{
"application": {
"application": "Application",
"build": {
"build_logs_of": "Créer des journaux de",
"finished_in": "Fini en",
"load_more": "Charger plus",
"no_logs": "Aucun journal trouvé",
"queued": "En file d'attente",
"queued_waiting_exec": "En file d'attente et en attente d'exécution.",
"running": "Fonctionnement",
"waiting_logs": "En attente des logs..."
},
"build_and_start_application": "Build et démarrer l'application",
"build_command": "Commande Build",
"build_logs": "Créer des journaux",
"build_pack": "Pack de Build",
"cant_activate_auto_deploy_without_repo": "Impossible d'activer les déploiements automatiques tant qu'une seule application n'est pas définie pour ce dépôt/branche.",
"configuration": {
"branch_already_in_use": "Cette branche est déjà utilisée par une autre application. \nLes webhooks ne fonctionneront pas dans ce cas pour les deux applications. \nÊtes-vous sûr de vouloir l'utiliser ?",
"buildpack": {
"choose_this_one": "Choisir celui-ci..."
},
"configuration_missing": "Configuration manquante",
"configure_build_pack": "Configurer le pack de build",
"configure_destination": "Configurer la destination",
"configure_it_now": "Configurez-le maintenant",
"found_lock_file": "Fichier .lock trouvé pour <span class=\"font-bold text-orange-500 pl-1\">{{packageManager}}</span>. \nL'utiliser pour les commandes prédéfinies.",
"loading_branches": "Chargement des branches...",
"loading_groups": "Chargement des groupes...",
"loading_projects": "Chargement des projets...",
"loading_repositories": "Chargement des dépôts Git...",
"no_branches_found": "Aucune branche trouvée",
"no_configurable_destination": "Aucune destination configurable trouvée",
"no_configurable_git": "Aucune source Git configurable trouvée",
"no_projects_found": "Aucun projet trouvé",
"no_repositories_configured": "Aucun dépôt Git configuré pour votre application.",
"scanning_repository_suggest_build_pack": "Analyse du dépôt pour vous suggérer un pack de Build...",
"select_a_branch": "Veuillez sélectionner une branche",
"select_a_git_source": "Sélectionnez une source Git",
"select_a_group": "Veuillez sélectionner un groupe",
"select_a_project": "Veuillez sélectionner un projet",
"select_a_repository": "Veuillez sélectionner un dépôt",
"select_a_repository_first": "Veuillez d'abord sélectionner un dépôt",
"select_a_repository_project": "Sélectionnez un dépôt / projet"
},
"configurations": "Configurations",
"confirm_to_delete": "Voulez-vous vraiment supprimer '{{name}}'?",
"debug_logs": "Journaux de débogage",
"delete_application": "Supprimer l'application",
"deployment_queued": "Déploiement en file d'attente.",
"destination": "Destination",
"directory_to_use_explainer": "Répertoire à utiliser comme base pour toutes les commandes.<br>Pourrait être utile avec <span class='text-green-500 font-bold'>monorepos</span>.",
"dns_not_set_error": "DNS non défini ou propagé pour {{domain}}.<br><br>Veuillez vérifier vos paramètres DNS.",
"dns_not_set_partial_error": "DNS non défini",
"domain_already_in_use": "Le domaine {{domain}} est déjà utilisé.",
"domain_fqdn": "Domaine (FQDN)",
"url_fqdn": "URL (FQDN)",
"enable_auto_deploy_webhooks": "Activez le déploiement automatique via des webhooks.",
"enable_automatic_deployment": "Activer le déploiement automatique",
"enable_debug_log_during_build": "Activez les journaux de débogage pendant la phase de build.<br><span class='text-red-500 font-bold'>Les informations sensibles</span> peuvent être visibles et enregistrées dans les journaux.",
"enable_mr_pr_previews": "Activer les aperçus MR/PR",
"enable_preview_deploy_mr_pr_requests": "Activez les déploiements de prévisualisation à partir de demandes d'extraction ou de fusion.",
"features": "Caractéristiques",
"git_repository": "Dépôt Git",
"git_source": "Source Git",
"https_explainer": "Si vous spécifiez <span class='text-green-500 font-bold'>https</span>, l'application sera accessible uniquement via https. \nUn certificat SSL sera généré pour vous.<br>Si vous spécifiez <span class='text-green-500 font-bold'>www</span>, l'application sera redirigée (302) à partir de non-www et vice versa \n.<br><br>Pour modifier le domaine, vous devez d'abord arrêter l'application.<br><br><span class='text-white font-bold'>Vous devez configurer, en avance, votre DNS pour pointer vers l'IP du serveur.</span>",
"install_command": "Commande d'installation",
"logs": "Journaux des applications",
"no_applications_found": "Aucune application trouvée",
"permission_denied_build_and_start_application": "Vous n'êtes pas autorisé à créer et à démarrer l'application.",
"permission_denied_delete_application": "Vous n'êtes pas autorisé à supprimer cette application",
"permission_denied_rebuild_application": "Vous n'êtes pas autorisé à re-build l'application.",
"permission_denied_stop_application": "Vous n'êtes pas autorisé à arrêter l'application.",
"persistent_storage": "Stockage persistant",
"preview": {
"need_during_buildtime": "Besoin pendant la build ?",
"no_previews_available": "Aucun aperçu disponible",
"redeploy": "Redéployer",
"setup_secret_app_first": "Vous pouvez ajouter des secrets aux déploiements PR/MR. \nVeuillez d'abord ajouter des secrets à l'application. \n<br>Utile pour créer des environnements <span class='text-green-500 font-bold'>de mise en scène</span>.",
"values_overwriting_app_secrets": "Ces valeurs remplacent les secrets d'application dans les déploiements PR/MR. \nUtile pour créer des environnements <span class='text-green-500 font-bold'>de mise en scène</span>."
},
"previews": "Aperçus",
"publish_directory_explainer": "Répertoire contenant tous les actifs à déployer. \n<br> Par exemple : <span class='text-green-500 font-bold'>dist</span>,<span class='text-green-500 font-bold'>_site</span> ou <span \nclass='text-green-500 font-bold'>public</span>.",
"rebuild_application": "Re-build l'application",
"secret": "secrets",
"secrets": {
"secret_saved": "Secret enregistré.",
"secrets_for": "secrets pour",
"use_isbuildsecret": "Utiliser isBuildSecret"
},
"settings_saved": "Paramètres sauvegardés.",
"ssl_explainer": "Il générera des certificats pour www et non-www. \n<br>Vous devez avoir <span class='font-bold text-green-500'>les deux entrées DNS</span> définies à l'avance.<br><br>Utile si vous prévoyez d'avoir des visiteurs sur les deux.",
"ssl_www_and_non_www": "Générer SSL pour www et non-www ?",
"start_command": "Démarrer la commande",
"stop_application": "Arrêter l'application",
"storage": {
"path_is_required": "Le chemin est requis.",
"persistent_storage_explainer": "Vous pouvez spécifier n'importe quel dossier que vous souhaitez conserver dans les déploiements. \n<br>Ceci est utile pour stocker des données telles qu'une base de données (SQLite) ou un cache.",
"storage_deleted": "Stockage supprimé.",
"storage_saved": "Stockage enregistré.",
"storage_updated": "Stockage mis à jour."
}
},
"database": {
"change_append_only_mode": "Changer le mode d'ajout uniquement",
"confirm_stop": "Êtes-vous sûr de vouloir arrêter {{name}} ?",
"connection_string": "Connexion string",
"default_database": "Base de données par défaut",
"delete_database": "Supprimer la base de données",
"generated_automatically_after_set_to_public": "Généré automatiquement après avoir été défini sur public",
"no_databases_found": "Aucune base de données trouvée",
"permission_denied_delete_database": "Vous n'êtes pas autorisé à supprimer une base de données",
"permission_denied_start_database": "Vous n'êtes pas autorisé à démarrer la base de données.",
"permission_denied_stop_database": "Vous n'êtes pas autorisé à arrêter la base de données.",
"select_database_type": "Sélectionnez un type de base de données",
"select_database_version": "Sélectionnez une version de la base de données",
"set_public": "Rendre public",
"start_database": "Démarrer la base de données",
"stop_database": "Arrêter la base de données",
"warning_append_only": "Utile si vous souhaitez restaurer des données Redis à partir d'une sauvegarde.<br><span class='font-bold text-white'>Le redémarrage de la base de données est nécessaire.</span>",
"warning_database_public": "Votre base de données sera accessible depuis Internet. \n<br>Prenez la sécurité au sérieux dans ce cas!"
},
"destination": {
"add_to_coolify": "Ajouter à Coolify",
"confirm_restart_proxy": "Voulez-vous vraiment redémarrer le proxy? \nTout sera reconfiguré en ~10 secondes.",
"coolify_proxy_restarting": "Redémarrage du Proxy Coolify...",
"coolify_proxy_started": "Proxy Coolify démarré!",
"coolify_proxy_stopped": "Proxy Coolify arrêté!",
"delete_destination": "Supprimer le destinataire",
"force_restart_proxy": "Forcer le redémarrage du proxy",
"new": {
"add_new_destination": "Ajouter une nouvelle destination",
"install_proxy": "Cela installera un proxy sur la destination pour vous permettre d'accéder à vos applications et services sans aucune configuration manuelle (recommandé pour Docker).<br><br>Les bases de données auront leur propre proxy.",
"predefined_destinations": "Destinations prédéfinies",
"saving_and_configuring_proxy": "Enregistrement et configuration du proxy..."
},
"new_error_network_already_exists": "Réseau {{network}} déjà configuré pour une autre équipe !",
"no_destination_found": "Aucune destination trouvée",
"permission_denied_delete_destination": "Vous n'êtes pas autorisé à supprimer cette destination",
"restarting_please_wait": "Redémarrage... veuillez patienter...",
"use_coolify_proxy": "Utiliser le Proxy Coolify ?"
},
"error": {
"here": "ici",
"you_are_lost": "Oups vous êtes perdu ! \nMais n'ayez pas peur !",
"you_can_find_your_way_back": "Tu peux retrouver ton chemin"
},
"forms": {
"action": "action",
"add": "Ajouter",
"already_used_for": "<span class=\"text-red-500\">{{type}}</span> déjà utilisé pour",
"api_port": "Port API",
"api_url": "URL de l'API",
"base_directory": "Répertoire de base",
"configuration": "Configuration",
"confirm_continue": "Êtes-vous sûr de continuer ?",
"default": "défaut",
"default_email_address": "Adresse e-mail par défaut",
"default_password": "Mot de passe par défaut",
"eg": "ex",
"email": "Adresse e-mail",
"engine": "Moteur",
"extra_config": "Configuration supplémentaire",
"generated_automatically_after_start": "Généré automatiquement après le démarrage",
"host": "Hôte",
"html_url": "URL HTML",
"ip_address": "Adresse IP",
"is_required": "est requis.",
"loading": "Chargement...",
"must_be_stopped_to_modify": "Doit être arrêté pour être modifié.",
"name": "Nom",
"network": "Réseau",
"new_password": "Nouveau mot de passe",
"no_actions_available": "Aucune action disponible",
"organization": "Organisation",
"password": "Mot de passe",
"password_again": "Mot de passe à nouveau",
"passwords_not_match": "Les mots de passe ne correspondent pas.",
"path": "Chemin",
"port": "Port",
"public_port_range": "Gamme de ports publics",
"public_port_range_explainer": "Ports utilisés pour exposer les bases de données/services/services internes.<br> Ajoutez-les à votre pare-feu (le cas échéant).<br><br>Vous pouvez spécifier une plage de ports, par exemple : <span class='text-yellow-500 \nfont-bold'>9000-9100</span>",
"publish_directory": "Publier le répertoire",
"remove": "Retirer",
"remove_domain": "Supprimer le domaine",
"removing": "Suppression...",
"root_db_password": "Mot de passe root de la base de données",
"root_db_user": "Utilisateur root de la base de données",
"root_user": "Utilisateur root",
"roots_password": "Mot de passe de l'utilisateur root",
"save": "sauvegarder",
"saving": "Sauvegarde...",
"select_a_service": "Sélectionnez un service",
"select_a_service_version": "Sélectionnez une version de service",
"set": "Régler",
"ssh_private_key": "Clé privée SSH",
"submit": "Nous faire parvenir",
"super_secure_new_password": "Nouveau mot de passe super sécurisé",
"type": "Taper",
"user": "Utilisateur",
"username": "Nom d'utilisateur",
"value": "Valeur",
"verify_emails_without_smtp": "Vérifier les e-mails sans SMTP",
"verifying": "Vérification",
"version": "Version"
},
"general": "Général",
"index": {
"applications": "Applications",
"dashboard": "Tableau de bord",
"database": "Base de données",
"databases": "Bases de données",
"destinations": "Destinations",
"git_sources": "Sources Git",
"global_settings": "Paramètres globaux",
"logout": "Se déconnecter",
"not_implemented_yet": "Pas encore implémenté",
"secret": "Secret",
"services": "Services",
"settings": "Réglages",
"team": "Équipe",
"teams": "Équipes"
},
"layout": {
"new_version": "Nouvelle version accessible. \nRechargement...",
"switch_to_a_different_team": "Changer d'équipe...",
"update_available": "Mise à jour disponible",
"update_done": "Mise à jour terminée.",
"wait_new_version_startup": "En attendant le lancement de la nouvelle version..."
},
"login": {
"already_logged_in": "Déjà connecté...",
"authenticating": "Authentification...",
"login": "Connexion"
},
"register": {
"first_user": "Vous enregistrez le premier utilisateur. \nCe sera l'administrateur de votre instance Coolify.",
"register": "S'inscrire"
},
"reset": {
"find_path_secret_key": "Vous pouvez le trouver dans ~/coolify/.env (COOLIFY_SECRET_KEY)",
"invalid_secret_key": "Clé secrète invalide.",
"reset_password": "Réinitialiser",
"secret_key": "Clef secrète"
},
"service": {
"delete_service": "Supprimer le service",
"no_service": "Aucun service trouvé",
"permission_denied_delete_service": "Vous n'êtes pas autorisé à supprimer un service.",
"permission_denied_start_service": "Vous n'êtes pas autorisé à démarrer le service.",
"permission_denied_stop_service": "Vous n'êtes pas autorisé à arrêter le service.",
"start_service": "Démarrer le service",
"stop_service": "Stopper le service"
},
"services": {
"all_email_verified": "Tous les e-mails sont vérifiés. \nVous pouvez vous connecter maintenant.",
"generate_www_non_www_ssl": "Il générera des certificats pour www et non-www. \n<br>Vous devez avoir <span class='font-bold text-pink-600'>les deux entrées DNS</span> définies à l'avance.<br><br>Le service devra être redémarré."
},
"setting": {
"coolify_proxy_settings": "Paramètres du proxy Coolify",
"credential_stat_explainer": "Identifiants pour la page <a class=\"text-white font-bold\" href=\"{{link}}\" target=\"_blank\">statistiques</a>.",
"domain_removed": "Domaine supprimé",
"must_remove_domain_before_changing": "Vous devez supprimer le domaine avant de pouvoir modifier ce paramètre.",
"permission_denied": "Vous n'avez pas la permission de faire cela. \n\\nDemandez à un administrateur de modifier vos autorisations.",
"registration_allowed": "Inscription autorisée ?",
"registration_allowed_explainer": "Autoriser d'autres inscriptions à l'application. \n<br>Il est désactivé après la première inscription.",
"ssl_explainer": "Si vous spécifiez <span class='text-yellow-500 font-bold'>https</span>, Coolify sera accessible uniquement via https. \nUn certificat SSL sera généré pour vous.<br>Si vous spécifiez <span class='text-yellow-500 font-bold'>www</span>, Coolify sera redirigé (302) à partir de non-www et vice versa."
},
"source": {
"application_id": "ID d'application",
"change_app_settings": "Modifier les paramètres de l'application {{name}}",
"create_new_app": "Créer une nouvelle application {{name}}",
"delete_git_source": "Supprimer la source Git",
"github": {
"redirecting": "Redirection vers Github..."
},
"gitlab": {
"already_configured": "L'application GitLab est déjà configurée.",
"gitlab_application_type": "Type d'application GitLab",
"group_owned": "Application détenue par le groupe",
"self_hosted": "Application à l'échelle de l'instance (auto-hébergée)",
"user_owned": "Application appartenant à l'utilisateur"
},
"group_name": "Nom de groupe",
"install_repositories": "Installer les dépôts",
"new": {
"git_source": "Ajouter une nouvelle source Git",
"official_providers": "Fournisseurs officiels"
},
"no_git_sources_found": "Aucune source git trouvée",
"oauth_id": "ID OAuth",
"oauth_id_explainer": "L'identifiant OAuth est l'identifiant unique de l'application GitLab. \n<br>Vous pouvez le trouver <span class='font-bold text-orange-600' >dans l'URL</span> de votre application GitLab OAuth.",
"permission_denied": "Vous n'êtes pas autorisé à supprimer une source Git",
"register_oauth_gitlab": "Enregistrer une nouvelle application OAuth sur GitLab"
},
"sources": {
"local_docker": "Docker local",
"organization_explainer": "Remplissez-le si vous souhaitez utiliser une organisation comme source Git. \nSinon, votre utilisateur sera utilisé.",
"remote_docker": "Station d'accueil à distance"
},
"team": {
"accept": "J'accepte",
"admin": "Administrateur",
"delete": "Supprimer",
"invite_new_member": "Inviter un nouveau membre",
"invite_only_register_explainer": "Vous ne pouvez inviter que des utilisateurs enregistrés pour le moment - sera bientôt prolongé.",
"invited_with_permissions": "Invité à <span class=\"font-bold text-pink-600\">{{teamName}}</span> avec <span class=\"font-bold text-rose-600\">{{permission}}</span \n> autorisation.",
"member": "membre(s)",
"members": "Membres",
"pending_invitation": "Invitation en attente",
"pending_invitations": "Invitations en attente",
"permission": "Autorisation",
"promote_to": "Promouvoir à {{grade}}",
"read": "Lire",
"revoke_invitation": "Révoquer l'invitation",
"root": "(suprême)",
"root_team_explainer": "Il s'agit de l'équipe <span class='text-red-500 font-bold'>suprême</span>. \nCela signifie que les membres de ce groupe peuvent gérer les paramètres à l'échelle de l'instance et avoir tous les privilèges dans Coolify (imaginez comme un utilisateur root sous Linux).",
"send_invitation": "Envoyer une invitation",
"you": "(Toi)"
}
}

View File

@@ -0,0 +1,42 @@
import { prisma } from '$lib/database';
import { buildQueue } from '.';
import got from 'got';
import { asyncExecShell, version } from '$lib/common';
import compare from 'compare-versions';
import { dev } from '$app/env';
export default async function (): Promise<void> {
try {
const currentVersion = version;
const { isAutoUpdateEnabled } = await prisma.setting.findFirst();
if (isAutoUpdateEnabled) {
const versions = await got
.get(
`https://get.coollabs.io/versions.json?appId=${process.env['COOLIFY_APP_ID']}&version=${currentVersion}`
)
.json();
const latestVersion = versions['coolify'].main.version;
const isUpdateAvailable = compare(latestVersion, currentVersion);
if (isUpdateAvailable === 1) {
const activeCount = await buildQueue.getActiveCount();
if (activeCount === 0) {
if (!dev) {
await buildQueue.pause();
console.log(`Updating Coolify to ${latestVersion}.`);
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
await asyncExecShell(`env | grep COOLIFY > .env`);
await asyncExecShell(
`docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-redis && docker rm coolify coolify-redis && docker compose up -d --force-recreate"`
);
} else {
await buildQueue.pause();
console.log('Updating (not really in dev mode).');
}
}
}
}
} catch (error) {
await buildQueue.resume();
console.log(error);
}
}

View File

@@ -20,28 +20,22 @@ import {
setDefaultConfiguration
} from '$lib/buildPacks/common';
import yaml from 'js-yaml';
import type { Job } from 'bullmq';
import type { BuilderJob } from '$lib/types/builderJob';
import type { ComposeFile } from '$lib/types/composeFile';
export default async function (job) {
let {
export default async function (job: Job<BuilderJob, void, string>): Promise<void> {
const {
id: applicationId,
repository,
branch,
buildPack,
name,
destinationDocker,
destinationDockerId,
gitSource,
build_id: buildId,
configHash,
port,
exposePort,
installCommand,
buildCommand,
startCommand,
fqdn,
baseDirectory,
publishDirectory,
projectId,
secrets,
phpModules,
@@ -52,7 +46,21 @@ export default async function (job) {
persistentStorage,
pythonWSGI,
pythonModule,
pythonVariable
pythonVariable,
denoOptions,
exposePort
} = job.data;
let {
branch,
buildPack,
port,
installCommand,
buildCommand,
startCommand,
baseDirectory,
publishDirectory,
dockerFileLocation,
denoMainFile
} = job.data;
const { debug } = settings;
@@ -68,7 +76,7 @@ export default async function (job) {
});
let imageId = applicationId;
let domain = getDomain(fqdn);
let volumes =
const volumes =
persistentStorage?.map((storage) => {
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${
buildPack !== 'docker' ? '/app' : ''
@@ -103,8 +111,10 @@ export default async function (job) {
buildCommand = configuration.buildCommand;
publishDirectory = configuration.publishDirectory;
baseDirectory = configuration.baseDirectory;
dockerFileLocation = configuration.dockerFileLocation;
denoMainFile = configuration.denoMainFile;
let commit = await importers[gitSource.type]({
const commit = await importers[gitSource.type]({
applicationId,
debug,
workdir,
@@ -179,6 +189,7 @@ export default async function (job) {
}
if (!imageFound || deployNeeded) {
await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId);
console.log(exposePort ? `${exposePort}:${port}` : port);
if (buildpacks[buildPack])
await buildpacks[buildPack]({
buildId,
@@ -197,7 +208,7 @@ export default async function (job) {
tag,
workdir,
docker,
port,
port: exposePort ? `${exposePort}:${port}` : port,
installCommand,
buildCommand,
startCommand,
@@ -206,15 +217,16 @@ export default async function (job) {
phpModules,
pythonWSGI,
pythonModule,
pythonVariable
pythonVariable,
dockerFileLocation,
denoMainFile,
denoOptions
});
else {
await saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId });
throw new Error(`Build pack ${buildPack} not found.`);
}
deployNeeded = true;
} else {
deployNeeded = false;
await saveBuildLog({ line: 'Nothing changed.', buildId, applicationId });
}
@@ -250,7 +262,7 @@ export default async function (job) {
repository,
branch,
projectId,
port,
port: exposePort ? `${exposePort}:${port}` : port,
commit,
installCommand,
buildCommand,
@@ -282,10 +294,21 @@ export default async function (job) {
volumes,
env_file: envFound ? [`${workdir}/.env`] : [],
networks: [docker.network],
ports: exposePort ? [`${exposePort}:${port}`] : [],
labels,
depends_on: [],
restart: 'always'
restart: 'always',
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
// logging: {
// driver: 'fluentd',
// },
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
}
},
networks: {

View File

@@ -1,11 +1,10 @@
import { dev } from '$app/env';
import { asyncExecShell, getEngine, version } from '$lib/common';
import { prisma } from '$lib/database';
import { defaultProxyImageHttp, defaultProxyImageTcp } from '$lib/haproxy';
export default async function () {
export default async function (): Promise<void> {
const destinationDockers = await prisma.destinationDocker.findMany();
for (const destinationDocker of destinationDockers) {
const host = getEngine(destinationDocker.engine);
const engines = [...new Set(destinationDockers.map(({ engine }) => engine))];
for (const engine of engines) {
const host = getEngine(engine);
// Cleanup old coolify images
try {
let { stdout: images } = await asyncExecShell(
@@ -16,56 +15,23 @@ export default async function () {
await asyncExecShell(`DOCKER_HOST=${host} docker rmi -f ${images}`);
}
} catch (error) {
console.log(error);
//console.log(error);
}
try {
await asyncExecShell(`DOCKER_HOST=${host} docker container prune -f`);
} catch (error) {
console.log(error);
//console.log(error);
}
try {
await asyncExecShell(`DOCKER_HOST=${host} docker image prune -f --filter "until=2h"`);
} catch (error) {
console.log(error);
//console.log(error);
}
// Cleanup old images older than a day
try {
await asyncExecShell(`DOCKER_HOST=${host} docker image prune --filter "until=72h" -a -f`);
} catch (error) {
//console.log(error);
}
// Tagging images with labels
// try {
// const images = [
// `coollabsio/${defaultProxyImageTcp}`,
// `coollabsio/${defaultProxyImageHttp}`,
// 'certbot/certbot:latest',
// 'node:16.14.0-alpine',
// 'alpine:latest',
// 'nginx:stable-alpine',
// 'node:lts',
// 'php:apache',
// 'rust:latest'
// ];
// for (const image of images) {
// try {
// await asyncExecShell(`DOCKER_HOST=${host} docker image inspect ${image}`);
// } catch (error) {
// await asyncExecShell(
// `DOCKER_HOST=${host} docker pull ${image} && echo "FROM ${image}" | docker build --label coolify.image="true" -t "${image}" -`
// );
// }
// }
// } catch (error) {}
// if (!dev) {
// // Cleanup images that are not managed by coolify
// try {
// await asyncExecShell(
// `DOCKER_HOST=${host} docker image prune --filter 'label!=coolify.image=true' -a -f`
// );
// } catch (error) {
// console.log(error);
// }
// // Cleanup old images >3 days
// try {
// await asyncExecShell(`DOCKER_HOST=${host} docker image prune --filter "until=72h" -a -f`);
// } catch (error) {
// console.log(error);
// }
// }
}
}

View File

@@ -1,6 +1,5 @@
import * as Bullmq from 'bullmq';
import { default as ProdBullmq, Job, QueueEvents, QueueScheduler } from 'bullmq';
import cuid from 'cuid';
import { default as ProdBullmq, QueueScheduler } from 'bullmq';
import { dev } from '$app/env';
import { prisma } from '$lib/database';
@@ -8,8 +7,10 @@ import builder from './builder';
import logger from './logger';
import cleanup from './cleanup';
import proxy from './proxy';
import proxyTcpHttp from './proxyTcpHttp';
import ssl from './ssl';
import sslrenewal from './sslrenewal';
import autoUpdater from './autoUpdater';
import { asyncExecShell, saveBuildLog } from '$lib/common';
@@ -28,22 +29,28 @@ const connectionOptions = {
}
};
const cron = async () => {
const cron = async (): Promise<void> => {
new QueueScheduler('proxy', connectionOptions);
new QueueScheduler('proxyTcpHttp', connectionOptions);
new QueueScheduler('cleanup', connectionOptions);
new QueueScheduler('ssl', connectionOptions);
new QueueScheduler('sslRenew', connectionOptions);
new QueueScheduler('autoUpdater', connectionOptions);
const queue = {
proxy: new Queue('proxy', { ...connectionOptions }),
proxyTcpHttp: new Queue('proxyTcpHttp', { ...connectionOptions }),
cleanup: new Queue('cleanup', { ...connectionOptions }),
ssl: new Queue('ssl', { ...connectionOptions }),
sslRenew: new Queue('sslRenew', { ...connectionOptions })
sslRenew: new Queue('sslRenew', { ...connectionOptions }),
autoUpdater: new Queue('autoUpdater', { ...connectionOptions })
};
await queue.proxy.drain();
await queue.proxyTcpHttp.drain();
await queue.cleanup.drain();
await queue.ssl.drain();
await queue.sslRenew.drain();
await queue.autoUpdater.drain();
new Worker(
'proxy',
@@ -55,6 +62,16 @@ const cron = async () => {
}
);
new Worker(
'proxyTcpHttp',
async () => {
await proxyTcpHttp();
},
{
...connectionOptions
}
);
new Worker(
'ssl',
async () => {
@@ -85,22 +102,22 @@ const cron = async () => {
}
);
new Worker(
'autoUpdater',
async () => {
await autoUpdater();
},
{
...connectionOptions
}
);
await queue.proxy.add('proxy', {}, { repeat: { every: 10000 } });
await queue.proxyTcpHttp.add('proxyTcpHttp', {}, { repeat: { every: 10000 } });
await queue.ssl.add('ssl', {}, { repeat: { every: dev ? 10000 : 60000 } });
if (!dev) await queue.cleanup.add('cleanup', {}, { repeat: { every: 300000 } });
await queue.sslRenew.add('sslRenew', {}, { repeat: { every: 1800000 } });
const events = {
proxy: new QueueEvents('proxy', { ...connectionOptions }),
ssl: new QueueEvents('ssl', { ...connectionOptions })
};
events.proxy.on('completed', (data) => {
// console.log(data)
});
events.ssl.on('completed', (data) => {
// console.log(data)
});
await queue.autoUpdater.add('autoUpdater', {}, { repeat: { every: 60000 } });
};
cron().catch((error) => {
console.log('cron failed to start');
@@ -113,6 +130,9 @@ const buildWorker = new Worker(buildQueueName, async (job) => await builder(job)
concurrency: 1,
...connectionOptions
});
buildQueue.resume().catch((err) => {
console.log('Build queue failed to resume!', err);
});
buildWorker.on('completed', async (job: Bullmq.Job) => {
try {
@@ -121,7 +141,6 @@ buildWorker.on('completed', async (job: Bullmq.Job) => {
setTimeout(async () => {
await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'success' } });
}, 1234);
console.log(error);
} finally {
const workdir = `/tmp/build-sources/${job.data.repository}/${job.data.build_id}`;
if (!dev) await asyncExecShell(`rm -fr ${workdir}`);
@@ -137,7 +156,6 @@ buildWorker.on('failed', async (job: Bullmq.Job, failedReason) => {
setTimeout(async () => {
await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'failed' } });
}, 1234);
console.log(error);
} finally {
const workdir = `/tmp/build-sources/${job.data.repository}`;
if (!dev) await asyncExecShell(`rm -fr ${workdir}`);

View File

@@ -1,7 +1,8 @@
import { prisma } from '$lib/database';
import { dev } from '$app/env';
import type { Job } from 'bullmq';
export default async function (job) {
export default async function (job: Job): Promise<void> {
const { line, applicationId, buildId } = job.data;
if (dev) console.debug(`[${applicationId}] ${line}`);
await prisma.buildLog.create({ data: { line, buildId, time: Number(job.id), applicationId } });

View File

@@ -1,7 +1,10 @@
import { ErrorHandler } from '$lib/database';
import { configureHAProxy } from '$lib/haproxy/configuration';
export default async function () {
export default async function (): Promise<void | {
status: number;
body: { message: string; error: string };
}> {
try {
return await configureHAProxy();
} catch (error) {

View File

@@ -0,0 +1,55 @@
import { ErrorHandler, generateDatabaseConfiguration, prisma } from '$lib/database';
import { startCoolifyProxy, startHttpProxy, startTcpProxy } from '$lib/haproxy';
export default async function (): Promise<void | {
status: number;
body: { message: string; error: string };
}> {
try {
// Coolify Proxy
const localDocker = await prisma.destinationDocker.findFirst({
where: { engine: '/var/run/docker.sock' }
});
if (localDocker && localDocker.isCoolifyProxyUsed) {
await startCoolifyProxy('/var/run/docker.sock');
}
// TCP Proxies
const databasesWithPublicPort = await prisma.database.findMany({
where: { publicPort: { not: null } },
include: { settings: true, destinationDocker: true }
});
for (const database of databasesWithPublicPort) {
const { destinationDockerId, destinationDocker, publicPort, id } = database;
if (destinationDockerId) {
const { privatePort } = generateDatabaseConfiguration(database);
await startTcpProxy(destinationDocker, id, publicPort, privatePort);
}
}
const wordpressWithFtp = await prisma.wordpress.findMany({
where: { ftpPublicPort: { not: null } },
include: { service: { include: { destinationDocker: true } } }
});
for (const ftp of wordpressWithFtp) {
const { service, ftpPublicPort } = ftp;
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId) {
await startTcpProxy(destinationDocker, `${id}-ftp`, ftpPublicPort, 22);
}
}
// HTTP Proxies
const minioInstances = await prisma.minio.findMany({
where: { publicPort: { not: null } },
include: { service: { include: { destinationDocker: true } } }
});
for (const minio of minioInstances) {
const { service, publicPort } = minio;
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId) {
await startHttpProxy(destinationDocker, id, publicPort, 9000);
}
}
} catch (error) {
return ErrorHandler(error.response?.body || error);
}
}

View File

@@ -1,6 +1,6 @@
import { generateSSLCerts } from '$lib/letsencrypt';
export default async function () {
export default async function (): Promise<void> {
try {
return await generateSSLCerts();
} catch (error) {

View File

@@ -1,13 +1,9 @@
import { asyncExecShell } from '$lib/common';
import { reloadHaproxy } from '$lib/haproxy';
export default async function () {
try {
await asyncExecShell(
`docker run --rm --name certbot-renewal -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs renew`
);
await reloadHaproxy('unix:///var/run/docker.sock');
} catch (error) {
throw error;
}
export default async function (): Promise<void> {
await asyncExecShell(
`docker run --rm --name certbot-renewal -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs renew`
);
await reloadHaproxy('unix:///var/run/docker.sock');
}

View File

@@ -1,8 +1,6 @@
export const publicPaths = [
'/login',
'/register',
'/reset',
'/reset/password',
'/webhooks/success',
'/webhooks/github',
'/webhooks/github/install',

View File

@@ -1,6 +1,8 @@
import { writable } from 'svelte/store';
import { writable, type Writable } from 'svelte/store';
export const gitTokens = writable({
githubToken: null,
gitlabToken: null
});
export const gitTokens: Writable<{ githubToken: string | null; gitlabToken: string | null }> =
writable({
githubToken: null,
gitlabToken: null
});
export const disabledButton: Writable<boolean> = writable(false);

25
src/lib/translations.ts Normal file
View File

@@ -0,0 +1,25 @@
import i18n from 'sveltekit-i18n';
import lang from './lang.json';
/** @type {import('sveltekit-i18n').Config} */
export const config = {
fallbackLocale: 'en',
translations: {
en: { lang },
fr: { lang }
},
loaders: [
{
locale: 'en',
key: '',
loader: async () => (await import('./locales/en.json')).default
},
{
locale: 'fr',
key: '',
loader: async () => (await import('./locales/fr.json')).default
}
]
};
export const { t, locales, locale, loadTranslations } = new i18n(config);

View File

@@ -0,0 +1,55 @@
import type { DestinationDocker, GithubApp, GitlabApp, GitSource, Secret } from '@prisma/client';
export type BuilderJob = {
build_id: string;
type: BuildType;
id: string;
name: string;
fqdn: string;
repository: string;
configHash: unknown;
branch: string;
buildPack: BuildPackName;
projectId: number;
port: number;
exposePort?: number;
installCommand: string;
buildCommand?: string;
startCommand?: string;
baseDirectory: string;
publishDirectory: string;
phpModules: string;
pythonWSGI: string;
pythonModule: string;
pythonVariable: string;
dockerFileLocation: string;
denoMainFile: string;
denoOptions: string;
createdAt: string;
updatedAt: string;
destinationDockerId: string;
destinationDocker: DestinationDocker;
gitSource: GitSource & { githubApp?: GithubApp; gitlabApp?: GitlabApp };
settings: BuilderJobSettings;
secrets: Secret[];
persistentStorage: { path: string }[];
pullmergeRequestId?: unknown;
sourceBranch?: string;
};
// TODO: Add the other build types
export type BuildType = 'manual';
// TODO: Add the other buildpack names
export type BuildPackName = 'node' | 'docker';
export type BuilderJobSettings = {
id: string;
applicationId: string;
dualCerts: boolean;
debug: boolean;
previews: boolean;
autodeploy: boolean;
createdAt: string;
updatedAt: string;
};

View File

@@ -18,11 +18,20 @@ export type ComposeFileService = {
restart: ComposeFileRestartOption;
depends_on?: string[];
command?: string;
ports?: string[];
build?: {
context: string;
dockerfile: string;
args?: Record<string, unknown>;
};
deploy?: {
restart_policy?: {
condition?: string;
delay?: string;
max_attempts?: number;
window?: string;
};
};
};
export type ComposerFileVersion =

View File

@@ -0,0 +1,8 @@
export type CreateDockerDestination = {
name: string;
engine: string;
remoteEngine: boolean;
network: string;
isCoolifyProxyUsed: boolean;
teamId: string;
};

View File

@@ -12,15 +12,18 @@
</script>
<script>
import { t } from '$lib/translations';
export let status;
export let error;
</script>
<div class="mx-auto flex h-screen flex-col items-center justify-center px-4">
<div class="pb-10 text-7xl font-bold">{status}</div>
<div class="text-3xl font-bold">Ooops you are lost! But don't be afraid!</div>
<div class="text-3xl font-bold">{$t('error.you_are_lost')}</div>
<div class="text-xl">
You can find your way back <a href="/" class="font-bold uppercase text-sky-400">here</a>
{$t('error.you_can_find_your_way_back')}
<a href="/" class="font-bold uppercase text-sky-400">{$t('error.here')}</a>
</div>
<div class="py-10 text-xs font-bold">
<pre

View File

@@ -1,8 +1,15 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
import { publicPaths } from '$lib/settings';
import { locale, loadTranslations } from '$lib/translations';
export const load: Load = async ({ fetch, url, session }) => {
const { pathname } = url;
const defaultLocale = 'en';
const sessionLocale = session.lang;
const initLocale = sessionLocale || locale.get() || defaultLocale;
await loadTranslations(initLocale, pathname);
if (!session.userId && !publicPaths.includes(url.pathname)) {
return {
status: 302,
@@ -39,7 +46,6 @@
import { asyncSleep } from '$lib/components/common';
import { del, get, post } from '$lib/api';
import { browser, dev } from '$app/env';
let isUpdateAvailable = false;
let updateStatus = {
@@ -176,7 +182,7 @@
<a
sveltekit:prefetch
href="/applications"
class="icons tooltip-right bg-coolgray-200 hover:text-green-500"
class="icons tooltip-green-500 tooltip-right bg-coolgray-200 hover:text-green-500"
class:text-green-500={$page.url.pathname.startsWith('/applications') ||
$page.url.pathname.startsWith('/new/application')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/applications') ||
@@ -204,7 +210,7 @@
<a
sveltekit:prefetch
href="/sources"
class="icons tooltip-right bg-coolgray-200 hover:text-orange-500"
class="icons tooltip-orange-500 tooltip-right bg-coolgray-200 hover:text-orange-500"
class:text-orange-500={$page.url.pathname.startsWith('/sources') ||
$page.url.pathname.startsWith('/new/source')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/sources') ||
@@ -234,7 +240,7 @@
<a
sveltekit:prefetch
href="/destinations"
class="icons tooltip-right bg-coolgray-200 hover:text-sky-500"
class="icons tooltip-sky-500 tooltip-right bg-coolgray-200 hover:text-sky-500"
class:text-sky-500={$page.url.pathname.startsWith('/destinations') ||
$page.url.pathname.startsWith('/new/destination')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/destinations') ||
@@ -269,7 +275,7 @@
<a
sveltekit:prefetch
href="/databases"
class="icons tooltip-right bg-coolgray-200 hover:text-purple-500"
class="icons tooltip-purple-500 tooltip-right bg-coolgray-200 hover:text-purple-500"
class:text-purple-500={$page.url.pathname.startsWith('/databases') ||
$page.url.pathname.startsWith('/new/database')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/databases') ||
@@ -296,7 +302,7 @@
<a
sveltekit:prefetch
href="/services"
class="icons tooltip-right bg-coolgray-200 hover:text-pink-500"
class="icons tooltip-pink-500 tooltip-right bg-coolgray-200 hover:text-pink-500"
class:text-pink-500={$page.url.pathname.startsWith('/services') ||
$page.url.pathname.startsWith('/new/service')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/services') ||
@@ -348,7 +354,7 @@
{:else if updateStatus.success === null}
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-8 h-9"
class="h-9 w-8"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
@@ -363,7 +369,7 @@
<line x1="16" y1="12" x2="12" y2="8" />
</svg>
{:else if updateStatus.success}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" class="w-8 h-9"
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" class="h-9 w-8"
><path
fill="#DD2E44"
d="M11.626 7.488c-.112.112-.197.247-.268.395l-.008-.008L.134 33.141l.011.011c-.208.403.14 1.223.853 1.937.713.713 1.533 1.061 1.936.853l.01.01L28.21 24.735l-.008-.009c.147-.07.282-.155.395-.269 1.562-1.562-.971-6.627-5.656-11.313-4.687-4.686-9.752-7.218-11.315-5.656z"
@@ -408,7 +414,7 @@
/></svg
>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" class="w-8 h-9"
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" class="h-9 w-8"
><path
fill="#FFCC4D"
d="M36 18c0 9.941-8.059 18-18 18S0 27.941 0 18 8.059 0 18 0s18 8.059 18 18"
@@ -435,7 +441,7 @@
<a
sveltekit:prefetch
href="/iam"
class="icons tooltip-right bg-coolgray-200 hover:text-fuchsia-500"
class="icons tooltip-fuchsia-500 tooltip-right bg-coolgray-200 hover:text-fuchsia-500"
class:text-fuchsia-500={$page.url.pathname.startsWith('/iam')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/iam')}
data-tooltip="IAM"
@@ -456,12 +462,11 @@
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85" />
</svg>
</a>
{#if $session.teamId === '0'}
<a
sveltekit:prefetch
href="/settings"
class="icons tooltip-right bg-coolgray-200 hover:text-yellow-500"
class="icons tooltip-yellow-500 tooltip-right bg-coolgray-200 hover:text-yellow-500"
class:text-yellow-500={$page.url.pathname.startsWith('/settings')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/settings')}
data-tooltip="Settings"
@@ -486,7 +491,7 @@
{/if}
<div
class="icons tooltip-right bg-coolgray-200 hover:text-red-500"
class="icons tooltip-red-500 tooltip-right bg-coolgray-200 hover:text-red-500"
data-tooltip="Logout"
on:click={logout}
>

View File

@@ -17,7 +17,7 @@
const endpoint = `/applications/${params.id}.json`;
const res = await fetch(endpoint);
if (res.ok) {
let { application, isRunning, appId, githubToken, gitlabToken } = await res.json();
let { application, isRunning, isExited, appId, githubToken, gitlabToken } = await res.json();
if (!application || Object.entries(application).length === 0) {
return {
status: 302,
@@ -46,6 +46,7 @@
props: {
application,
isRunning,
isExited,
githubToken,
gitlabToken
},
@@ -67,27 +68,39 @@
<script lang="ts">
export let application;
export let isRunning;
export let isExited;
export let githubToken;
export let gitlabToken;
import { page, session } from '$app/stores';
import { errorNotification } from '$lib/form';
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import Loading from '$lib/components/Loading.svelte';
import { del, post } from '$lib/api';
import { del, get, post } from '$lib/api';
import { goto } from '$app/navigation';
import { gitTokens } from '$lib/store';
import { toast } from '@zerodevx/svelte-toast';
import { disabledButton } from '$lib/store';
import { onDestroy, onMount } from 'svelte';
import { t } from '$lib/translations';
if (githubToken) $gitTokens.githubToken = githubToken;
if (gitlabToken) $gitTokens.gitlabToken = gitlabToken;
let loading = false;
let statusInterval;
$disabledButton =
!$session.isAdmin ||
!application.fqdn ||
!application.gitSource ||
!application.repository ||
!application.destinationDocker ||
!application.buildPack;
const { id } = $page.params;
async function handleDeploySubmit() {
try {
const { buildId } = await post(`/applications/${id}/deploy.json`, { ...application });
toast.push('Deployment queued.');
toast.push($t('application.deployment_queued'));
if ($page.url.pathname.startsWith(`/applications/${id}/logs/build`)) {
return window.location.assign(`/applications/${id}/logs/build?buildId=${buildId}`);
} else {
@@ -101,7 +114,7 @@
}
async function deleteApplication(name) {
const sure = confirm(`Are you sure you would like to delete '${name}'?`);
const sure = confirm($t('application.confirm_to_delete', { name }));
if (sure) {
loading = true;
try {
@@ -121,142 +134,86 @@
return errorNotification(error);
}
}
async function getStatus() {
statusInterval = setInterval(async () => {
const data = await get(`/applications/${id}.json`);
isRunning = data.isRunning;
isExited = data.isExited;
}, 1000);
}
onDestroy(() => {
clearInterval(statusInterval);
});
onMount(async () => {
await getStatus();
});
</script>
<nav class="nav-side">
{#if loading}
<Loading fullscreen cover />
{:else}
{#if application.fqdn && application.gitSource && application.repository && application.destinationDocker && application.buildPack}
{#if isRunning}
{#if isExited}
<a
href={!$disabledButton ? `/applications/${id}/logs` : null}
class=" icons bg-transparent tooltip-bottom text-sm flex items-center text-red-500 tooltip-red-500"
data-tooltip="Application exited with an error!"
sveltekit:prefetch
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentcolor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M8.7 3h6.6c.3 0 .5 .1 .7 .3l4.7 4.7c.2 .2 .3 .4 .3 .7v6.6c0 .3 -.1 .5 -.3 .7l-4.7 4.7c-.2 .2 -.4 .3 -.7 .3h-6.6c-.3 0 -.5 -.1 -.7 -.3l-4.7 -4.7c-.2 -.2 -.3 -.4 -.3 -.7v-6.6c0 -.3 .1 -.5 .3 -.7l4.7 -4.7c.2 -.2 .4 -.3 .7 -.3z"
/>
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
</a>
{/if}
{#if isRunning}
<button
on:click={stopApplication}
title="Stop application"
type="submit"
disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-red-500"
data-tooltip={$session.isAdmin
? $t('application.stop_application')
: $t('application.permission_denied_stop_application')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="6" y="5" width="4" height="14" rx="1" />
<rect x="14" y="5" width="4" height="14" rx="1" />
</svg>
</button>
<form on:submit|preventDefault={handleDeploySubmit}>
<button
on:click={stopApplication}
title="Stop application"
title="Rebuild application"
type="submit"
disabled={!$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-red-500"
disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 hover:text-green-500"
data-tooltip={$session.isAdmin
? 'Stop application'
: 'You do not have permission to stop the application.'}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="6" y="5" width="4" height="14" rx="1" />
<rect x="14" y="5" width="4" height="14" rx="1" />
</svg>
</button>
<form on:submit|preventDefault={handleDeploySubmit}>
<button
title="Rebuild application"
type="submit"
disabled={!$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 hover:text-green-500"
data-tooltip={$session.isAdmin
? 'Rebuild application'
: 'You do not have permission to rebuild application.'}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M16.3 5h.7a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h5l-2.82 -2.82m0 5.64l2.82 -2.82"
transform="rotate(-45 12 12)"
/>
</svg>
</button>
</form>
{:else}
<form on:submit|preventDefault={handleDeploySubmit}>
<button
title="Build and start application"
type="submit"
disabled={!$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-green-500"
data-tooltip={$session.isAdmin
? 'Build and start application'
: 'You do not have permission to Build and start application.'}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
</button>
</form>
{/if}
<div class="border border-stone-700 h-8" />
<a
href="/applications/{id}"
sveltekit:prefetch
class="hover:text-yellow-500 rounded"
class:text-yellow-500={$page.url.pathname === `/applications/${id}`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}`}
>
<button
title="Configurations"
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
data-tooltip="Configurations"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="4" y="8" width="4" height="4" />
<line x1="6" y1="4" x2="6" y2="8" />
<line x1="6" y1="12" x2="6" y2="20" />
<rect x="10" y="14" width="4" height="4" />
<line x1="12" y1="4" x2="12" y2="14" />
<line x1="12" y1="18" x2="12" y2="20" />
<rect x="16" y="5" width="4" height="4" />
<line x1="18" y1="4" x2="18" y2="5" />
<line x1="18" y1="9" x2="18" y2="20" />
</svg></button
></a
>
<a
href="/applications/{id}/secrets"
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/applications/${id}/secrets`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/secrets`}
>
<button
title="Secret"
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
data-tooltip="Secret"
? 'Rebuild application'
: 'You do not have permission to rebuild application.'}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -270,24 +227,22 @@
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3"
d="M16.3 5h.7a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h5l-2.82 -2.82m0 5.64l2.82 -2.82"
transform="rotate(-45 12 12)"
/>
<circle cx="12" cy="11" r="1" />
<line x1="12" y1="12" x2="12" y2="14.5" />
</svg></button
></a
>
<a
href="/applications/{id}/storage"
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/applications/${id}/storage`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/storage`}
>
</svg>
</button>
</form>
{:else}
<form on:submit|preventDefault={handleDeploySubmit}>
<button
title="Persistent Storage"
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
data-tooltip="Persistent Storage"
title="Build and start application"
type="submit"
disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-green-500"
data-tooltip={$session.isAdmin
? 'Build and start application'
: 'You do not have permission to Build and start application.'}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -300,122 +255,223 @@
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<ellipse cx="12" cy="6" rx="8" ry="3" />
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
<path d="M7 4v16l13 -8z" />
</svg>
</button></a
>
<a
href="/applications/{id}/previews"
sveltekit:prefetch
class="hover:text-orange-500 rounded"
class:text-orange-500={$page.url.pathname === `/applications/${id}/previews`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/previews`}
>
<button
title="Previews"
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
data-tooltip="Previews"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="7" cy="18" r="2" />
<circle cx="7" cy="6" r="2" />
<circle cx="17" cy="12" r="2" />
<line x1="7" y1="8" x2="7" y2="16" />
<path d="M7 8a4 4 0 0 0 4 4h4" />
</svg></button
></a
>
<div class="border border-stone-700 h-8" />
<a
href="/applications/{id}/logs"
sveltekit:prefetch
class="hover:text-sky-500 rounded"
class:text-sky-500={$page.url.pathname === `/applications/${id}/logs`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs`}
>
<button
title="Application Logs"
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500 "
data-tooltip="Application Logs"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 19a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<path d="M3 6a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<line x1="3" y1="6" x2="3" y2="19" />
<line x1="12" y1="6" x2="12" y2="19" />
<line x1="21" y1="6" x2="21" y2="19" />
</svg>
</button></a
>
<a
href="/applications/{id}/logs/build"
sveltekit:prefetch
class="hover:text-red-500 rounded"
class:text-red-500={$page.url.pathname === `/applications/${id}/logs/build`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs/build`}
>
<button
title="Build Logs"
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500 "
data-tooltip="Build Logs"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="19" cy="13" r="2" />
<circle cx="4" cy="17" r="2" />
<circle cx="13" cy="17" r="2" />
<line x1="13" y1="19" x2="4" y2="19" />
<line x1="4" y1="15" x2="13" y2="15" />
<path d="M8 12v-5h2a3 3 0 0 1 3 3v5" />
<path d="M5 15v-2a1 1 0 0 1 1 -1h7" />
<path d="M19 11v-7l-6 7" />
</svg>
</button></a
>
<div class="border border-stone-700 h-8" />
</button>
</form>
{/if}
<div class="border border-coolgray-500 h-8" />
<a
href={!$disabledButton ? `/applications/${id}` : null}
sveltekit:prefetch
class="hover:text-yellow-500 rounded"
class:text-yellow-500={$page.url.pathname === `/applications/${id}`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}`}
>
<button
title="Configurations"
disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm"
data-tooltip="Configurations"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="4" y="8" width="4" height="4" />
<line x1="6" y1="4" x2="6" y2="8" />
<line x1="6" y1="12" x2="6" y2="20" />
<rect x="10" y="14" width="4" height="4" />
<line x1="12" y1="4" x2="12" y2="14" />
<line x1="12" y1="18" x2="12" y2="20" />
<rect x="16" y="5" width="4" height="4" />
<line x1="18" y1="4" x2="18" y2="5" />
<line x1="18" y1="9" x2="18" y2="20" />
</svg></button
></a
>
<a
href={!$disabledButton ? `/applications/${id}/secrets` : null}
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/applications/${id}/secrets`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/secrets`}
>
<button
title="Secret"
disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm"
data-tooltip="Secret"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3"
/>
<circle cx="12" cy="11" r="1" />
<line x1="12" y1="12" x2="12" y2="14.5" />
</svg></button
></a
>
<a
href={!$disabledButton ? `/applications/${id}/storage` : null}
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/applications/${id}/storage`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/storage`}
>
<button
title="Persistent Storage"
disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm"
data-tooltip="Persistent Storage"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<ellipse cx="12" cy="6" rx="8" ry="3" />
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
</svg>
</button></a
>
<a
href={!$disabledButton ? `/applications/${id}/previews` : null}
sveltekit:prefetch
class="hover:text-orange-500 rounded"
class:text-orange-500={$page.url.pathname === `/applications/${id}/previews`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/previews`}
>
<button
title="Previews"
disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm"
data-tooltip="Previews"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="7" cy="18" r="2" />
<circle cx="7" cy="6" r="2" />
<circle cx="17" cy="12" r="2" />
<line x1="7" y1="8" x2="7" y2="16" />
<path d="M7 8a4 4 0 0 0 4 4h4" />
</svg></button
></a
>
<div class="border border-coolgray-500 h-8" />
<a
href={!$disabledButton ? `/applications/${id}/logs` : null}
sveltekit:prefetch
class="hover:text-sky-500 rounded"
class:text-sky-500={$page.url.pathname === `/applications/${id}/logs`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs`}
>
<button
title={$t('application.logs')}
disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm"
data-tooltip={$t('application.logs')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 19a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<path d="M3 6a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<line x1="3" y1="6" x2="3" y2="19" />
<line x1="12" y1="6" x2="12" y2="19" />
<line x1="21" y1="6" x2="21" y2="19" />
</svg>
</button></a
>
<a
href={!$disabledButton ? `/applications/${id}/logs/build` : null}
sveltekit:prefetch
class="hover:text-red-500 rounded"
class:text-red-500={$page.url.pathname === `/applications/${id}/logs/build`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs/build`}
>
<button
title="Build Logs"
disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm"
data-tooltip="Build Logs"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="19" cy="13" r="2" />
<circle cx="4" cy="17" r="2" />
<circle cx="13" cy="17" r="2" />
<line x1="13" y1="19" x2="4" y2="19" />
<line x1="4" y1="15" x2="13" y2="15" />
<path d="M8 12v-5h2a3 3 0 0 1 3 3v5" />
<path d="M5 15v-2a1 1 0 0 1 1 -1h7" />
<path d="M19 11v-7l-6 7" />
</svg>
</button></a
>
<div class="border border-coolgray-500 h-8" />
<button
on:click={() => deleteApplication(application.name)}
title="Delete application"
title={$t('application.delete_application')}
type="submit"
disabled={!$session.isAdmin}
class:hover:text-red-500={$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm"
data-tooltip={$session.isAdmin
? 'Delete application'
: 'You do not have permission to delete this application'}
? $t('application.delete_application')
: $t('application.permission_denied_delete_application')}
>
<DeleteIcon />
</button>

View File

@@ -5,6 +5,7 @@ import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
import { promises as dns } from 'dns';
import getPort from 'get-port';
import { t } from '$lib/translations';
export const post: RequestHandler = async (event) => {
const { status, body } = await getUserDetails(event);
@@ -19,7 +20,9 @@ export const post: RequestHandler = async (event) => {
const found = await db.isDomainConfigured({ id, fqdn });
if (found) {
throw {
message: `Domain ${getDomain(fqdn).replace('www.', '')} is already used.`
message: t.get('application.domain_already_in_use', {
domain: getDomain(fqdn).replace('www.', '')
})
};
}
if (!dev && !forceSave) {
@@ -37,7 +40,7 @@ export const post: RequestHandler = async (event) => {
if (localIp?.length > 0) {
if (ip?.length === 0 || !ip.includes(localIp[0])) {
throw {
message: `DNS not set or propogated for ${domain}.<br><br>Please check your DNS settings.`
message: t.get('application.dns_not_set_error', { domain: domain })
};
}
}

View File

@@ -5,6 +5,7 @@
import { post } from '$lib/api';
import { findBuildPack } from '$lib/components/templates';
import { errorNotification } from '$lib/form';
import { t } from '$lib/translations';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
@@ -19,13 +20,14 @@
const tempBuildPack = JSON.parse(
JSON.stringify(findBuildPack(buildPack.name, packageManager))
);
delete tempBuildPack.name;
delete tempBuildPack.fancyName;
delete tempBuildPack.color;
delete tempBuildPack.hoverColor;
if (foundConfig.buildPack !== name) {
await post(`/applications/${id}.json`, { ...tempBuildPack });
await post(`/applications/${id}.json`, { ...tempBuildPack, buildPack: name });
}
await post(`/applications/${id}/configuration/buildpack.json`, { buildPack: name });
return await goto(from || `/applications/${id}`);
@@ -42,7 +44,9 @@
buildPack.name && buildPack.color}"
><span>{buildPack.fancyName}</span>
{#if !scanning && foundConfig?.name === buildPack.name}
<span class="absolute bottom-0 pb-2 text-xs">Choose this one...</span>
<span class="absolute bottom-0 pb-2 text-xs"
>{$t('application.configuration.buildpack.choose_this_one')}</span
>
{/if}
</button>
</form>

View File

@@ -7,6 +7,7 @@
import { errorNotification } from '$lib/form';
import { onMount } from 'svelte';
import { gitTokens } from '$lib/store';
import { t } from '$lib/translations';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
@@ -36,8 +37,15 @@
});
}
async function loadBranchesByPage(page = 0) {
return await get(`${apiUrl}/repos/${selected.repository}/branches?per_page=100&page=${page}`, {
Authorization: `token ${$gitTokens.githubToken}`
});
}
let reposSelectOptions;
let branchSelectOptions;
async function loadRepositories() {
let page = 1;
let reposCount = 0;
@@ -58,24 +66,28 @@
}));
}
async function loadBranches(event) {
branches = [];
selected.repository = event.detail.value;
loading.branches = true;
selected.branch = undefined;
selected.projectId = repositories.find((repo) => repo.full_name === selected.repository).id;
try {
branches = await get(`${apiUrl}/repos/${selected.repository}/branches`, {
Authorization: `token ${$gitTokens.githubToken}`
});
branchSelectOptions = branches.map((branch) => ({
value: branch.name,
label: branch.name
}));
return;
} catch ({ error }) {
return errorNotification(error);
} finally {
loading.branches = false;
let page = 1;
let branchCount = 0;
loading.branches = true;
const loadedBranches = await loadBranchesByPage();
branches = branches.concat(loadedBranches);
branchCount = branches.length;
if (branchCount === 100) {
while (branchCount === 100) {
page = page + 1;
const nextBranches = await loadBranchesByPage(page);
branches = branches.concat(nextBranches);
branchCount = nextBranches.length;
}
}
loading.branches = false;
branchSelectOptions = branches.map((branch) => ({
value: branch.name,
label: branch.name
}));
}
async function isBranchAlreadyUsed(event) {
selected.branch = event.detail.value;
@@ -84,9 +96,7 @@
`/applications/${id}/configuration/repository.json?repository=${selected.repository}&branch=${selected.branch}`
);
if (data.used) {
const sure = confirm(
`This branch is already used by another application. Webhooks won't work in this case for both applications. Are you sure you want to use it?`
);
const sure = confirm($t('application.configuration.branch_already_in_use'));
if (sure) {
selected.autodeploy = false;
showSave = true;
@@ -160,36 +170,44 @@
{#if repositories.length === 0 && loading.repositories === false}
<div class="flex-col text-center">
<div class="pb-4">No repositories configured for your Git Application.</div>
<a href={`/sources/${application.gitSource.id}`}><button>Configure it now</button></a>
<div class="pb-4">{$t('application.configuration.no_repositories_configured')}</div>
<a href={`/sources/${application.gitSource.id}`}
><button>{$t('application.configuration.configure_it_now')}</button></a
>
</div>
{:else}
<form on:submit|preventDefault={handleSubmit} class="flex flex-col justify-center text-center">
<div class="flex-col space-y-3 md:space-y-0 space-x-1">
<div class="flex gap-4">
<div class="flex-col md:flex gap-4">
<div class="custom-select-wrapper">
<Select
placeholder={loading.repositories
? 'Loading repositories ...'
: 'Please select a repository'}
? $t('application.configuration.loading_repositories')
: $t('application.configuration.select_a_repository')}
id="repository"
showIndicator={true}
isWaiting={loading.repositories}
on:select={loadBranches}
items={reposSelectOptions}
isDisabled={loading.repositories}
isClearable={false}
/>
</div>
<input class="hidden" bind:value={selected.projectId} name="projectId" />
<div class="custom-select-wrapper">
<Select
placeholder={loading.branches
? 'Loading branches ...'
? $t('application.configuration.loading_branches')
: !selected.repository
? 'Please select a repository first'
: 'Please select a branch'}
id="repository"
? $t('application.configuration.select_a_repository_first')
: $t('application.configuration.select_a_branch')}
isWaiting={loading.branches}
showIndicator={selected.repository}
id="branches"
on:select={isBranchAlreadyUsed}
items={branchSelectOptions}
isDisabled={loading.branches || !selected.repository}
isClearable={false}
/>
</div>
</div>
@@ -200,15 +218,8 @@
type="submit"
disabled={!showSave}
class:bg-orange-600={showSave}
class:hover:bg-orange-500={showSave}>Save</button
class:hover:bg-orange-500={showSave}>{$t('forms.save')}</button
>
<!-- <button class="w-40"
><a
class="no-underline"
href="{apiUrl}/apps/{application.gitSource.githubApp.name}/installations/new"
>Modify Repositories</a
></button
> -->
</div>
</form>
{/if}

View File

@@ -9,6 +9,7 @@
import { goto } from '$app/navigation';
import { del, get, post, put } from '$lib/api';
import { gitTokens } from '$lib/store';
import { t } from '$lib/translations';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
@@ -139,9 +140,7 @@
`/applications/${id}/configuration/repository.json?repository=${selected.project.path_with_namespace}&branch=${selected.branch.name}`
);
if (data.used) {
const sure = confirm(
`This branch is already used by another application. Webhooks won't work in this case for both applications. Are you sure you want to use it?`
);
const sure = confirm($t('application.configuration.branch_already_in_use'));
if (sure) {
autodeploy = false;
showSave = true;
@@ -270,11 +269,11 @@
<div class="flex flex-col space-y-2 px-4 xl:flex-row xl:space-y-0 xl:space-x-2 ">
{#if loading.base}
<select name="group" disabled class="w-96">
<option selected value="">Loading groups...</option>
<option selected value="">{$t('application.configuration.loading_groups')}</option>
</select>
{:else}
<select name="group" class="w-96" bind:value={selected.group} on:change={loadProjects}>
<option value="" disabled selected>Please select a group</option>
<option value="" disabled selected>{$t('application.configuration.select_a_group')}</option>
{#each groups as group}
<option value={group}>{group.full_name}</option>
{/each}
@@ -282,7 +281,7 @@
{/if}
{#if loading.projects}
<select name="project" disabled class="w-96">
<option selected value="">Loading projects...</option>
<option selected value="">{$t('application.configuration.loading_projects')}</option>
</select>
{:else if !loading.projects && projects.length > 0}
<select
@@ -292,20 +291,24 @@
on:change={loadBranches}
disabled={!selected.group}
>
<option value="" disabled selected>Please select a project</option>
<option value="" disabled selected
>{$t('application.configuration.select_a_project')}</option
>
{#each projects as project}
<option value={project}>{project.name}</option>
{/each}
</select>
{:else}
<select name="project" disabled class="w-96">
<option disabled selected value="">No projects found</option>
<option disabled selected value=""
>{$t('application.configuration.no_projects_found')}</option
>
</select>
{/if}
{#if loading.branches}
<select name="branch" disabled class="w-96">
<option selected value="">Loading branches...</option>
<option selected value="">{$t('application.configuration.loading_branches')}</option>
</select>
{:else if !loading.branches && branches.length > 0}
<select
@@ -315,14 +318,17 @@
on:change={isBranchAlreadyUsed}
disabled={!selected.project}
>
<option value="" disabled selected>Please select a branch</option>
<option value="" disabled selected>{$t('application.configuration.select_a_branch')}</option
>
{#each branches as branch}
<option value={branch}>{branch.name}</option>
{/each}
</select>
{:else}
<select name="project" disabled class="w-96">
<option disabled selected value="">No branches found</option>
<option disabled selected value=""
>{$t('application.configuration.no_branches_found')}</option
>
</select>
{/if}
</div>
@@ -334,7 +340,7 @@
disabled={!showSave || loading.save}
class:bg-orange-600={showSave && !loading.save}
class:hover:bg-orange-500={showSave && !loading.save}
>{loading.save ? 'Saving...' : 'Save'}</button
>{loading.save ? $t('forms.saving') : $t('forms.save')}</button
>
</div>
</form>

View File

@@ -31,11 +31,12 @@
import { buildPacks, findBuildPack, scanningTemplates } from '$lib/components/templates';
import BuildPack from './_BuildPack.svelte';
import { page, session } from '$app/stores';
import { page } from '$app/stores';
import { get } from '$lib/api';
import { errorNotification } from '$lib/form';
import { gitTokens } from '$lib/store';
import { browser } from '$app/env';
import { t } from '$lib/translations';
const { id } = $page.params;
@@ -204,24 +205,27 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Configure Build Pack</div>
<div class="mr-4 text-2xl tracking-tight">
{$t('application.configuration.configure_build_pack')}
</div>
</div>
{#if scanning}
<div class="flex justify-center space-x-1 p-6 font-bold">
<div class="text-xl tracking-tight">Scanning repository to suggest a build pack for you...</div>
<div class="text-xl tracking-tight">
{$t('application.configuration.scanning_repository_suggest_build_pack')}
</div>
</div>
{:else}
{#if packageManager === 'yarn' || packageManager === 'pnpm'}
<div class="flex justify-center p-6">
Found lock file for <span class="font-bold text-orange-500 pl-1">{packageManager}</span>.
Using it for predefined commands commands.
{@html $t('application.configuration.found_lock_file', { packageManager })}
</div>
{/if}
<div class="max-w-7xl mx-auto flex flex-wrap justify-center">
{#each buildPacks as buildPack}
<div class="p-2">
<BuildPack {buildPack} {scanning} {packageManager} bind:foundConfig />
<BuildPack {buildPack} {scanning} bind:foundConfig />
</div>
{/each}
</div>

View File

@@ -33,6 +33,7 @@
import { errorNotification } from '$lib/form';
import { goto } from '$app/navigation';
import { post } from '$lib/api';
import { t } from '$lib/translations';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
@@ -60,12 +61,14 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Configure Destination</div>
<div class="mr-4 text-2xl tracking-tight">
{$t('application.configuration.configure_destination')}
</div>
</div>
<div class="flex flex-col justify-center">
{#if !destinations || ownDestinations.length === 0}
<div class="flex-col">
<div class="pb-2">No configurable Destination found</div>
<div class="pb-2">{$t('application.configuration.no_configurable_destination')}</div>
<div class="flex justify-center">
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
<svg

View File

@@ -18,6 +18,8 @@
</script>
<script lang="ts">
import { t } from '$lib/translations';
export let application;
export let appId;
@@ -26,7 +28,9 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Select a Repository / Project</div>
<div class="mr-4 text-2xl tracking-tight">
{$t('application.configuration.select_a_repository_project')}
</div>
</div>
<div class="flex flex-wrap justify-center">
{#if application.gitSource.type === 'github'}

View File

@@ -33,6 +33,7 @@
import { errorNotification } from '$lib/form';
import { goto } from '$app/navigation';
import { post } from '$lib/api';
import { t } from '$lib/translations';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
@@ -72,12 +73,14 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Select a Git Source</div>
<div class="mr-4 text-2xl tracking-tight">
{$t('application.configuration.select_a_git_source')}
</div>
</div>
<div class="flex flex-col justify-center">
{#if !filteredSources || ownSources.length === 0}
<div class="flex-col">
<div class="pb-2 text-center">No configurable Git Source found</div>
<div class="pb-2 text-center">{$t('application.configuration.no_configurable_git')}</div>
<div class="flex justify-center">
<button on:click={newSource} class="add-icon bg-orange-600 hover:bg-orange-500">
<svg
@@ -139,7 +142,7 @@
<div class="font-bold text-xl text-center truncate">{source.name}</div>
{#if source.gitlabApp && !source.gitlabAppId}
<div class="font-bold text-center truncate text-red-500 group-hover:text-white">
Configuration missing
{$t('application.configuration.configuration_missing')}
</div>
{/if}
</button>

View File

@@ -36,6 +36,7 @@ export const post: RequestHandler = async (event) => {
data: {
id: buildId,
applicationId: id,
branch: applicationFound.branch,
destinationDockerId: applicationFound.destinationDocker.id,
gitSourceId: applicationFound.gitSource.id,
githubAppId: applicationFound.gitSource.githubApp?.id,

View File

@@ -1,7 +1,7 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import { checkContainer } from '$lib/haproxy';
import { checkContainer, isContainerExited } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit';
import jsonwebtoken from 'jsonwebtoken';
import { get as getRequest } from '$lib/api';
@@ -16,17 +16,20 @@ export const get: RequestHandler = async (event) => {
const appId = process.env['COOLIFY_APP_ID'];
let isRunning = false;
let isExited = false;
let githubToken = event.locals.cookies?.githubToken || null;
let gitlabToken = event.locals.cookies?.gitlabToken || null;
try {
const application = await db.getApplication({ id, teamId });
if (application.destinationDockerId) {
isRunning = await checkContainer(application.destinationDocker.engine, id);
isExited = await isContainerExited(application.destinationDocker.engine, id);
}
return {
status: 200,
body: {
isRunning,
isExited,
application,
appId,
githubToken,
@@ -58,16 +61,16 @@ export const post: RequestHandler = async (event) => {
publishDirectory,
pythonWSGI,
pythonModule,
pythonVariable
pythonVariable,
dockerFileLocation,
denoMainFile,
denoOptions
} = await event.request.json();
if (port) port = Number(port);
if (exposePort) {
exposePort = Number(exposePort);
const publicPort = await getPort({ port: exposePort });
if (exposePort !== publicPort) {
exposePort = -1;
}
}
if (denoOptions) denoOptions = denoOptions.trim();
try {
const defaultConfiguration = await setDefaultConfiguration({
@@ -77,7 +80,9 @@ export const post: RequestHandler = async (event) => {
startCommand,
buildCommand,
publishDirectory,
baseDirectory
baseDirectory,
dockerFileLocation,
denoMainFile
});
await db.configureApplication({
id,
@@ -94,6 +99,9 @@ export const post: RequestHandler = async (event) => {
pythonWSGI,
pythonModule,
pythonVariable,
dockerFileLocation,
denoMainFile,
denoOptions,
...defaultConfiguration
});
return { status: 201 };

View File

@@ -48,6 +48,8 @@
import { post } from '$lib/api';
import cuid from 'cuid';
import { browser } from '$app/env';
import { disabledButton } from '$lib/store';
import { t } from '$lib/translations';
const { id } = $page.params;
let domainEl: HTMLInputElement;
@@ -68,11 +70,6 @@
value: 'Gunicorn',
label: 'Gunicorn'
}
// },
// {
// value: 'uWSGI',
// label: 'uWSGI'
// }
];
if (browser && window.location.hostname === 'demo.coolify.io' && !application.fqdn) {
@@ -105,7 +102,7 @@
branch: application.branch,
projectId: application.projectId
});
return toast.push('Settings saved.');
return toast.push($t('application.settings_saved'));
} catch ({ error }) {
if (name === 'debug') {
debug = !debug;
@@ -131,9 +128,10 @@
exposePort: application.exposePort
});
await post(`/applications/${id}.json`, { ...application });
return window.location.reload();
$disabledButton = false;
return toast.push('Configurations saved.');
} catch ({ error }) {
if (error?.startsWith('DNS not set')) {
if (error?.startsWith($t('application.dns_not_set_partial_error'))) {
forceSave = true;
}
return errorNotification(error);
@@ -176,6 +174,29 @@
</svg></a
>
{/if}
{#if application.exposePort}
<a
href={`${window.location.origin.split(':').slice(0, 2).join(':')}:${application.exposePort}`}
target="_blank"
class="icons tooltip-bottom flex items-center bg-transparent text-sm"
><svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg></a
>
{/if}
<a
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
@@ -223,7 +244,7 @@
<!-- svelte-ignore missing-declaration -->
<form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex space-x-1 pb-5 font-bold">
<div class="title">General</div>
<div class="title">{$t('general')}</div>
{#if $session.isAdmin}
<button
type="submit"
@@ -232,13 +253,17 @@
class:hover:bg-green-500={!loading}
class:hover:bg-orange-400={forceSave}
disabled={loading}
>{loading ? 'Saving...' : forceSave ? 'Are you sure to continue?' : 'Save'}</button
>{loading
? $t('forms.saving')
: forceSave
? $t('forms.confirm_continue')
: $t('forms.save')}</button
>
{/if}
</div>
<div class="grid grid-flow-row gap-2 px-10">
<div class="mt-2 grid grid-cols-2 items-center">
<label for="name" class="text-base font-bold text-stone-100">Name</label>
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
<input
readonly={!$session.isAdmin}
name="name"
@@ -248,7 +273,9 @@
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="gitSource" class="text-base font-bold text-stone-100">Git Source</label>
<label for="gitSource" class="text-base font-bold text-stone-100"
>{$t('application.git_source')}</label
>
<a
href={$session.isAdmin
? `/applications/${id}/configuration/source?from=/applications/${id}`
@@ -263,7 +290,9 @@
>
</div>
<div class="grid grid-cols-2 items-center">
<label for="repository" class="text-base font-bold text-stone-100">Git Repository</label>
<label for="repository" class="text-base font-bold text-stone-100"
>{$t('application.git_repository')}</label
>
<a
href={$session.isAdmin
? `/applications/${id}/configuration/repository?from=/applications/${id}&to=/applications/${id}/configuration/buildpack`
@@ -278,7 +307,9 @@
>
</div>
<div class="grid grid-cols-2 items-center">
<label for="buildPack" class="text-base font-bold text-stone-100">Build Pack</label>
<label for="buildPack" class="text-base font-bold text-stone-100"
>{$t('application.build_pack')}</label
>
<a
href={$session.isAdmin
? `/applications/${id}/configuration/buildpack?from=/applications/${id}`
@@ -294,7 +325,9 @@
>
</div>
<div class="grid grid-cols-2 items-center pb-8">
<label for="destination" class="text-base font-bold text-stone-100">Destination</label>
<label for="destination" class="text-base font-bold text-stone-100"
>{$t('application.destination')}</label
>
<div class="no-underline">
<input
value={application.destinationDocker.name}
@@ -306,20 +339,20 @@
</div>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">Application</div>
<div class="title">{$t('application.application')}</div>
</div>
<div class="grid grid-flow-row gap-2 px-10">
<div class="grid grid-cols-2">
<div class="flex-col">
<label for="fqdn" class="pt-2 text-base font-bold text-stone-100">URL (FQDN)</label>
<label for="fqdn" class="pt-2 text-base font-bold text-stone-100"
>{$t('application.url_fqdn')}</label
>
{#if browser && window.location.hostname === 'demo.coolify.io'}
<Explainer
text="<span class='text-white font-bold'>You can use the predefined random url name or enter your own domain name.</span>"
/>
{/if}
<Explainer
text="If you specify <span class='text-green-500 font-bold'>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-green-500 font-bold'>www</span>, the application will be redirected (302) from non-www and vice versa.<br><br>To modify the url, you must first stop the application.<br><br><span class='text-white font-bold'>You must set your DNS to point to the server IP in advance.</span>"
/>
<Explainer text={$t('application.https_explainer')} />
</div>
<input
readonly={!$session.isAdmin || isRunning}
@@ -334,12 +367,12 @@
</div>
<div class="grid grid-cols-2 items-center pb-8">
<Setting
dataTooltip="Must be stopped to modify."
dataTooltip={$t('forms.must_be_stopped_to_modify')}
disabled={isRunning}
isCenter={false}
bind:setting={dualCerts}
title="Generate SSL for www and non-www?"
description="It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-green-500'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both."
title={$t('application.ssl_www_and_non_www')}
description={$t('application.ssl_explainer')}
on:click={() => !isRunning && changeSettings('dualCerts')}
/>
</div>
@@ -378,13 +411,13 @@
{/if}
{#if !staticDeployments.includes(application.buildPack)}
<div class="grid grid-cols-2 items-center">
<label for="port" class="text-base font-bold text-stone-100">Port</label>
<label for="port" class="text-base font-bold text-stone-100">{$t('forms.port')}</label>
<input
readonly={!$session.isAdmin}
name="port"
id="port"
bind:value={application.port}
placeholder={application.buildPack === 'python' ? '8000' : '3000'}
placeholder="{$t('forms.default')}: 'python' ? '8000' : '3000'"
/>
</div>
{/if}
@@ -403,63 +436,105 @@
{#if !notNodeDeployments.includes(application.buildPack)}
<div class="grid grid-cols-2 items-center">
<label for="installCommand" class="text-base font-bold text-stone-100"
>Install Command</label
>{$t('application.install_command')}</label
>
<input
readonly={!$session.isAdmin}
name="installCommand"
id="installCommand"
bind:value={application.installCommand}
placeholder="default: yarn install"
placeholder="{$t('forms.default')}: yarn install"
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="buildCommand" class="text-base font-bold text-stone-100">Build Command</label>
<label for="buildCommand" class="text-base font-bold text-stone-100"
>{$t('application.build_command')}</label
>
<input
readonly={!$session.isAdmin}
name="buildCommand"
id="buildCommand"
bind:value={application.buildCommand}
placeholder="default: yarn build"
placeholder="{$t('forms.default')}: yarn build"
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="startCommand" class="text-base font-bold text-stone-100">Start Command</label>
<label for="startCommand" class="text-base font-bold text-stone-100"
>{$t('application.start_command')}</label
>
<input
readonly={!$session.isAdmin}
name="startCommand"
id="startCommand"
bind:value={application.startCommand}
placeholder="default: yarn start"
placeholder="{$t('forms.default')}: yarn start"
/>
</div>
{/if}
{#if application.buildPack === 'docker'}
<div class="grid grid-cols-2 items-center">
<label for="dockerFileLocation" class="text-base font-bold text-stone-100"
>Dockerfile Location</label
>
<input
readonly={!$session.isAdmin}
name="dockerFileLocation"
id="dockerFileLocation"
bind:value={application.dockerFileLocation}
placeholder="default: /Dockerfile"
/>
<Explainer
text="Does not rely on Base Directory. <br>Should be absolute path, like <span class='text-green-500 font-bold'>/data/Dockerfile</span> or <span class='text-green-500 font-bold'>/Dockerfile.</span>"
/>
</div>
{/if}
{#if application.buildPack === 'deno'}
<div class="grid grid-cols-2 items-center">
<label for="denoMainFile" class="text-base font-bold text-stone-100">Main File</label>
<input
readonly={!$session.isAdmin}
name="denoMainFile"
id="denoMainFile"
bind:value={application.denoMainFile}
placeholder="default: main.ts"
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="denoOptions" class="text-base font-bold text-stone-100">Arguments</label>
<input
readonly={!$session.isAdmin}
name="denoOptions"
id="denoOptions"
bind:value={application.denoOptions}
placeholder="eg: --allow-net --allow-hrtime --config path/to/file.json"
/>
<Explainer
text="List of arguments to pass to <span class='text-green-500 font-bold'>deno run</span> command. Could include permissions, configurations files, etc."
/>
</div>
{/if}
<div class="grid grid-cols-2 items-center">
<div class="flex-col">
<label for="baseDirectory" class="pt-2 text-base font-bold text-stone-100"
>Base Directory</label
>{$t('forms.base_directory')}</label
>
<Explainer
text="Directory to use as the base for all commands.<br>Could be useful with <span class='text-green-500 font-bold'>monorepos</span>."
/>
<Explainer text={$t('application.directory_to_use_explainer')} />
</div>
<input
readonly={!$session.isAdmin}
name="baseDirectory"
id="baseDirectory"
bind:value={application.baseDirectory}
placeholder="default: /"
placeholder="{$t('forms.default')}: /"
/>
</div>
{#if !notNodeDeployments.includes(application.buildPack)}
<div class="grid grid-cols-2 items-center">
<div class="flex-col">
<label for="publishDirectory" class="pt-2 text-base font-bold text-stone-100"
>Publish Directory</label
>{$t('forms.publish_directory')}</label
>
<Explainer
text="Directory containing all the assets for deployment. <br> For example: <span class='text-green-500 font-bold'>dist</span>,<span class='text-green-500 font-bold'>_site</span> or <span class='text-green-500 font-bold'>public</span>."
/>
<Explainer text={$t('application.publish_directory_explainer')} />
</div>
<input
@@ -467,14 +542,14 @@
name="publishDirectory"
id="publishDirectory"
bind:value={application.publishDirectory}
placeholder=" default: /"
placeholder=" {$t('forms.default')}: /"
/>
</div>
{/if}
</div>
</form>
<div class="flex space-x-1 pb-5 font-bold">
<div class="title">Features</div>
<div class="title">{$t('application.features')}</div>
</div>
<div class="px-10 pb-10">
<div class="grid grid-cols-2 items-center">
@@ -482,8 +557,8 @@
isCenter={false}
bind:setting={autodeploy}
on:click={() => changeSettings('autodeploy')}
title="Enable Automatic Deployment"
description="Enable automatic deployment through webhooks."
title={$t('application.enable_automatic_deployment')}
description={$t('application.enable_auto_deploy_webhooks')}
/>
</div>
<div class="grid grid-cols-2 items-center">
@@ -491,8 +566,8 @@
isCenter={false}
bind:setting={previews}
on:click={() => changeSettings('previews')}
title="Enable MR/PR Previews"
description="Enable preview deployments from pull or merge requests."
title={$t('application.enable_mr_pr_previews')}
description={$t('application.enable_preview_deploy_mr_pr_requests')}
/>
</div>
<div class="grid grid-cols-2 items-center">
@@ -500,8 +575,8 @@
isCenter={false}
bind:setting={debug}
on:click={() => changeSettings('debug')}
title="Debug Logs"
description="Enable debug logs during build phase.<br><span class='text-red-500 font-bold'>Sensitive information</span> could be visible and saved in logs."
title={$t('application.debug_logs')}
description={$t('application.enable_debug_log_during_build')}
/>
</div>
</div>

View File

@@ -10,6 +10,7 @@
import LoadingLogs from '../_Loading.svelte';
import { get } from '$lib/api';
import { errorNotification } from '$lib/form';
import { t } from '$lib/translations';
let logs = [];
let loading = true;
@@ -84,7 +85,7 @@
<LoadingLogs />
{/if}
{#if currentStatus === 'queued'}
<div class="text-center font-bold text-xl">Queued and waiting for execution.</div>
<div class="text-center font-bold text-xl">{$t('application.build.queued_waiting_exec')}</div>
{:else}
<div class="flex justify-end sticky top-0 p-2">
<button

View File

@@ -21,12 +21,12 @@
<script lang="ts">
import { page } from '$app/stores';
import { changeQueryParams, dateOptions, getDomain } from '$lib/components/common';
import { changeQueryParams, dateOptions } from '$lib/components/common';
import BuildLog from './_BuildLog.svelte';
import { get } from '$lib/api';
import { errorNotification } from '$lib/form';
import { goto } from '$app/navigation';
import { t } from '$lib/translations';
export let builds;
export let application;
@@ -87,7 +87,9 @@
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">Build Logs</div>
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
{$t('application.build_logs')}
</div>
<span class="text-xs">{application.name} </span>
</div>
@@ -174,7 +176,7 @@
>
<div class="flex-col px-2">
<div class="text-sm font-bold">
{application.branch}
{build.branch || application.branch}
</div>
<div class="text-xs">
{build.type}
@@ -184,12 +186,14 @@
<div class="w-48 text-center text-xs">
{#if build.status === 'running'}
<div class="font-bold">Running</div>
<div class="font-bold">{$t('application.build.running')}</div>
{:else if build.status === 'queued'}
<div class="font-bold">Queued</div>
<div class="font-bold">{$t('application.build.queued')}</div>
{:else}
<div>{build.since}</div>
<div>Finished in <span class="font-bold">{build.took}s</span></div>
<div>
{$t('application.build.finished_in')} <span class="font-bold">{build.took}s</span>
</div>
{/if}
</div>
</div>
@@ -198,7 +202,8 @@
{#if !noMoreBuilds}
{#if buildCount > 5}
<div class="flex space-x-2">
<button disabled={noMoreBuilds} class="w-full" on:click={loadMoreBuilds}>Load More</button
<button disabled={noMoreBuilds} class="w-full" on:click={loadMoreBuilds}
>{$t('application.build.load_more')}</button
>
</div>
{/if}
@@ -213,5 +218,5 @@
</div>
</div>
{#if buildCount === 0}
<div class="text-center text-xl font-bold">No logs found</div>
<div class="text-center text-xl font-bold">{$t('application.build.no_logs')}</div>
{/if}

View File

@@ -10,6 +10,10 @@ export const get: RequestHandler = async (event) => {
if (status === 401) return { status, body };
const { id } = event.params;
let since = event.url.searchParams.get('since') || 0;
if (since !== 0) {
since = dayjs(since).unix();
}
try {
const { destinationDockerId, destinationDocker } = await db.prisma.application.findUnique({
where: { id },
@@ -20,13 +24,22 @@ export const get: RequestHandler = async (event) => {
try {
const container = await docker.engine.getContainer(id);
if (container) {
const logs = (
await container.logs({
stdout: true,
stderr: true,
timestamps: true,
since,
tail: 5000
})
)
.toString()
.split('\n')
.map((l) => l.slice(8))
.filter((a) => a);
return {
body: {
logs: (await container.logs({ stdout: true, stderr: true, timestamps: true }))
.toString()
.split('\n')
.map((l) => l.slice(8))
.filter((a) => a)
logs
}
};
}

View File

@@ -24,19 +24,21 @@
export let application;
import { page } from '$app/stores';
import LoadingLogs from './_Loading.svelte';
import { getDomain } from '$lib/components/common';
import { get } from '$lib/api';
import { errorNotification } from '$lib/form';
import { t } from '$lib/translations';
let loadLogsInterval = null;
let logs = [];
let followingBuild;
let lastLog = null;
let followingInterval;
let followingLogs;
let logsEl;
let position = 0;
const { id } = $page.params;
onMount(async () => {
loadLogs();
loadAllLogs();
loadLogsInterval = setInterval(() => {
loadLogs();
}, 1000);
@@ -45,25 +47,53 @@
clearInterval(loadLogsInterval);
clearInterval(followingInterval);
});
async function loadAllLogs() {
try {
const data: any = await get(`/applications/${id}/logs.json`);
if (data?.logs) {
lastLog = data.logs[data.logs.length - 1];
logs = data.logs;
}
} catch (error) {
console.log(error);
return errorNotification(error);
}
}
async function loadLogs() {
try {
const newLogs = await get(`/applications/${id}/logs.json`);
logs = newLogs.logs;
return;
} catch ({ error }) {
const newLogs: any = await get(
`/applications/${id}/logs.json?since=${lastLog?.split(' ')[0] || 0}`
);
if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) {
logs = logs.concat(newLogs.logs);
lastLog = newLogs.logs[newLogs.logs.length - 1];
}
} catch (error) {
return errorNotification(error);
}
}
function detect() {
if (position < logsEl.scrollTop) {
position = logsEl.scrollTop;
} else {
if (followingLogs) {
clearInterval(followingInterval);
followingLogs = false;
}
position = logsEl.scrollTop;
}
}
function followBuild() {
followingBuild = !followingBuild;
if (followingBuild) {
followingLogs = !followingLogs;
if (followingLogs) {
followingInterval = setInterval(() => {
logsEl.scrollTop = logsEl.scrollHeight;
window.scrollTo(0, document.body.scrollHeight);
}, 100);
}, 1000);
} else {
window.clearInterval(followingInterval);
clearInterval(followingInterval);
}
}
</script>
@@ -142,16 +172,19 @@
</div>
<div class="flex flex-row justify-center space-x-2 px-10 pt-6">
{#if logs.length === 0}
<div class="text-xl font-bold tracking-tighter">Waiting for the logs...</div>
<div class="text-xl font-bold tracking-tighter">{$t('application.build.waiting_logs')}</div>
{:else}
<div class="relative w-full">
<LoadingLogs />
<div class="flex justify-end sticky top-0 p-2">
<div class="text-right " />
{#if loadLogsInterval}
<LoadingLogs />
{/if}
<div class="flex justify-end sticky top-0 p-2 mx-1">
<button
on:click={followBuild}
class="bg-transparent"
data-tooltip="Follow logs"
class:text-green-500={followingBuild}
class:text-green-500={followingLogs}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -174,8 +207,9 @@
<div
class="font-mono w-full leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
bind:this={logsEl}
on:scroll={detect}
>
<div class="px-2">
<div class="px-2 pr-14">
{#each logs as log}
{log + '\n'}
{/each}

View File

@@ -30,6 +30,7 @@
import Explainer from '$lib/components/Explainer.svelte';
import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast';
import { t } from '$lib/translations';
const { id } = $page.params;
async function refreshSecrets() {
@@ -134,10 +135,12 @@
<table class="mx-auto border-separate text-left">
<thead>
<tr class="h-12">
<th scope="col">Name</th>
<th scope="col">Value</th>
<th scope="col" class="w-64 text-center">Need during buildtime?</th>
<th scope="col" class="w-96 text-center">Action</th>
<th scope="col">{$t('forms.name')}</th>
<th scope="col">{$t('forms.value')}</th>
<th scope="col" class="w-64 text-center"
>{$t('application.preview.need_during_buildtime')}</th
>
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th>
</tr>
</thead>
<tbody>
@@ -171,13 +174,15 @@
</a>
<div class="flex items-center justify-center">
<button class="bg-coollabs hover:bg-coollabs-100" on:click={() => redeploy(container)}
>Redeploy</button
>{$t('application.preview.redeploy')}</button
>
</div>
{/each}
{:else}
<div class="flex-col">
<div class="text-center font-bold text-xl">No previews available</div>
<div class="text-center font-bold text-xl">
{$t('application.preview.no_previews_available')}
</div>
</div>
{/if}
</div>

View File

@@ -6,6 +6,7 @@
import { saveSecret } from './utils';
import pLimit from 'p-limit';
import { createEventDispatcher } from 'svelte';
import { t } from '$lib/translations';
const dispatch = createEventDispatcher();
let batchSecrets = '';
@@ -38,11 +39,11 @@
}
</script>
<h2 class="title my-6 font-bold">Paste .env file</h2>
<h2 class="title my-6 font-bold">{$t('application.secret__batch_dot_env')}</h2>
<form on:submit|preventDefault={getValues} class="mb-12 w-full">
<textarea bind:value={batchSecrets} class="mb-2 min-h-[200px] w-full" />
<button
class="bg-green-600 hover:bg-green-500 disabled:text-white disabled:opacity-40"
type="submit">Batch add secrets</button
type="submit">{$t('application.batch_secrets')}</button
>
</form>

View File

@@ -12,6 +12,7 @@
import { del } from '$lib/api';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { errorNotification } from '$lib/form';
import { t } from '$lib/translations';
import { toast } from '@zerodevx/svelte-toast';
import { createEventDispatcher } from 'svelte';
import { saveSecret } from './utils';
@@ -104,7 +105,7 @@
class:cursor-not-allowed={isPRMRSecret}
class:cursor-pointer={!isPRMRSecret}
>
<span class="sr-only">Use isBuildSecret</span>
<span class="sr-only">{$t('application.secrets.use_isbuildsecret')}</span>
<span
class="pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow transition duration-200 ease-in-out"
class:translate-x-5={isBuildSecret}
@@ -145,17 +146,19 @@
{#if isNewSecret}
<div class="flex items-center justify-center">
<button class="bg-green-600 hover:bg-green-500" on:click={() => createSecret(true)}
>Add</button
>{$t('forms.add')}</button
>
</div>
{:else}
<div class="flex flex-row justify-center space-x-2">
<div class="flex items-center justify-center">
<button class="" on:click={() => createSecret(false)}>Set</button>
<button class="" on:click={() => createSecret(false)}>{$t('forms.set')}</button>
</div>
{#if !isPRMRSecret}
<div class="flex justify-center items-end">
<button class="bg-red-600 hover:bg-red-500" on:click={removeSecret}>Remove</button>
<button class="bg-red-600 hover:bg-red-500" on:click={removeSecret}
>{$t('forms.remove')}</button
>
</div>
{/if}
</div>

View File

@@ -25,6 +25,7 @@
import pLimit from 'p-limit';
import Secret from './_Secret.svelte';
import { page } from '$app/stores';
import { t } from '$lib/translations';
import { get } from '$lib/api';
import { saveSecret } from './utils';
import { toast } from '@zerodevx/svelte-toast';
@@ -61,14 +62,13 @@
await refreshSecrets();
toast.push('Secrets saved');
}
function asd() {
console.log(secrets);
}
</script>
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">Secrets</div>
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
{$t('application.secret')}
</div>
<span class="text-xs">{application.name} </span>
</div>
@@ -140,10 +140,12 @@
<table class="mx-auto border-separate text-left">
<thead>
<tr class="h-12">
<th scope="col">Name</th>
<th scope="col">Value</th>
<th scope="col" class="w-64 text-center">Need during buildtime?</th>
<th scope="col" class="w-96 text-center">Action</th>
<th scope="col">{$t('forms.name')}</th>
<th scope="col">{$t('forms.value')}</th>
<th scope="col" class="w-64 text-center"
>{$t('application.preview.need_during_buildtime')}</th
>
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th>
</tr>
</thead>
<tbody>
@@ -164,7 +166,6 @@
</tr>
</tbody>
</table>
<button on:click={asd}>Save</button>
<h2 class="title my-6 font-bold">Paste .env file</h2>
<form on:submit|preventDefault={getValues} class="mb-12 w-full">
<textarea bind:value={batchSecrets} class="mb-2 min-h-[200px] w-full" />

View File

@@ -1,6 +1,7 @@
import { toast } from '@zerodevx/svelte-toast';
import { errorNotification } from '$lib/form';
import { post } from '$lib/api';
import { t } from '$lib/translations';
type Props = {
isNew: boolean;
@@ -21,8 +22,8 @@ export async function saveSecret({
isNewSecret,
applicationId
}: Props): Promise<void> {
if (!name) return errorNotification('Name is required.');
if (!value) return errorNotification('Value is required.');
if (!name) return errorNotification(`${t.get('forms.name')} ${t.get('forms.is_required')}`);
if (!value) return errorNotification(`${t.get('forms.value')} ${t.get('forms.is_required')}`);
try {
await post(`/applications/${applicationId}/secrets.json`, {
name,

View File

@@ -1,6 +1,7 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import { t } from '$lib/translations';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
@@ -14,8 +15,7 @@ export const post: RequestHandler = async (event) => {
const isDouble = await db.checkDoubleBranch(branch, projectId);
if (isDouble && autodeploy) {
throw {
message:
'Cannot activate automatic deployments until only one application is defined for this repository / branch.'
message: t.get('application.cant_activate_auto_deploy_without_repo')
};
}
await db.setApplicationSettings({ id, debug, previews, dualCerts, autodeploy });

View File

@@ -10,12 +10,13 @@
import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast';
import { t } from '$lib/translations';
const { id } = $page.params;
const dispatch = createEventDispatcher();
async function saveStorage(newStorage = false) {
try {
if (!storage.path) return errorNotification('Path is required.');
if (!storage.path) return errorNotification($t('application.storage.path_is_required'));
storage.path = storage.path.startsWith('/') ? storage.path : `/${storage.path}`;
storage.path = storage.path.endsWith('/') ? storage.path.slice(0, -1) : storage.path;
storage.path.replace(/\/\//g, '/');
@@ -29,8 +30,8 @@
storage.path = null;
storage.id = null;
}
if (newStorage) toast.push('Storage saved.');
else toast.push('Storage updated.');
if (newStorage) toast.push($t('application.storage.storage_saved'));
else toast.push($t('application.storage.storage_updated'));
} catch ({ error }) {
return errorNotification(error);
}
@@ -39,7 +40,7 @@
try {
await del(`/applications/${id}/storage.json`, { path: storage.path });
dispatch('refresh');
toast.push('Storage deleted.');
toast.push($t('application.storage.storage_deleted'));
} catch ({ error }) {
return errorNotification(error);
}
@@ -57,16 +58,19 @@
<td>
{#if isNew}
<div class="flex items-center justify-center">
<button class="bg-green-600 hover:bg-green-500" on:click={() => saveStorage(true)}>Add</button
<button class="bg-green-600 hover:bg-green-500" on:click={() => saveStorage(true)}
>{$t('forms.add')}</button
>
</div>
{:else}
<div class="flex flex-row justify-center space-x-2">
<div class="flex items-center justify-center">
<button class="" on:click={() => saveStorage(false)}>Set</button>
<button class="" on:click={() => saveStorage(false)}>{$t('forms.set')}</button>
</div>
<div class="flex justify-center items-end">
<button class="bg-red-600 hover:bg-red-500" on:click={removeStorage}>Remove</button>
<button class="bg-red-600 hover:bg-red-500" on:click={removeStorage}
>{$t('forms.remove')}</button
>
</div>
</div>
{/if}

View File

@@ -28,6 +28,7 @@
import Storage from './_Storage.svelte';
import { get } from '$lib/api';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations';
const { id } = $page.params;
async function refreshStorage() {
@@ -111,15 +112,12 @@
<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={'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.'}
/>
<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">
<th scope="col">Path</th>
<th scope="col">{$t('forms.path')}</th>
</tr>
</thead>
<tbody>

View File

@@ -3,6 +3,8 @@
import { session } from '$app/stores';
import { post } from '$lib/api';
import { goto } from '$app/navigation';
import { t } from '$lib/translations';
import { getDomain } from '$lib/components/common';
import Rust from '$lib/components/svg/applications/Rust.svelte';
import Nodejs from '$lib/components/svg/applications/Nodejs.svelte';
@@ -19,7 +21,7 @@
import Docker from '$lib/components/svg/applications/Docker.svelte';
import Astro from '$lib/components/svg/applications/Astro.svelte';
import Eleventy from '$lib/components/svg/applications/Eleventy.svelte';
import { getDomain } from '$lib/components/common';
import Deno from '$lib/components/svg/applications/Deno.svelte';
async function newApplication() {
const { id } = await post('/applications/new', {});
@@ -38,7 +40,7 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl ">Applications</div>
<div class="mr-4 text-2xl ">{$t('index.applications')}</div>
{#if $session.isAdmin}
<div on:click={newApplication} class="add-icon cursor-pointer bg-green-600 hover:bg-green-500">
<svg
@@ -60,7 +62,7 @@
<div class="flex flex-col flex-wrap justify-center">
{#if !applications || ownApplications.length === 0}
<div class="flex-col">
<div class="text-center text-xl font-bold">No applications found</div>
<div class="text-center text-xl font-bold">{$t('application.no_applications_found')}</div>
</div>
{/if}
{#if ownApplications.length > 0 || otherApplications.length > 0}
@@ -100,6 +102,8 @@
<Astro />
{:else if application.buildPack.toLowerCase() === 'eleventy'}
<Eleventy />
{:else if application.buildPack.toLowerCase() === 'deno'}
<Deno />
{/if}
{/if}
@@ -156,6 +160,8 @@
<Astro />
{:else if application.buildPack.toLowerCase() === 'eleventy'}
<Eleventy />
{:else if application.buildPack.toLowerCase() === 'deno'}
<Deno />
{/if}
{/if}

View File

@@ -1,4 +1,4 @@
import { getTeam, getUserDetails } from '$lib/common';
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
@@ -57,7 +57,8 @@ export const post: RequestHandler = async (event) => {
headers: {
'set-cookie': [
`${cookie}=${value}; HttpOnly; Path=/; Max-Age=15778800;`,
'gitlabToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'
'gitlabToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT',
'githubToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'
],
Location: from
}

View File

@@ -1,6 +1,7 @@
<script>
export let database;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { t } from '$lib/translations';
</script>
<div class="flex space-x-1 py-5 font-bold">
@@ -8,34 +9,38 @@
</div>
<div class="space-y-2 px-10">
<div class="grid grid-cols-2 items-center">
<label for="defaultDatabase" class="text-base font-bold text-stone-100">Default Database</label>
<label for="defaultDatabase" class="text-base font-bold text-stone-100"
>{$t('database.default_database')}</label
>
<CopyPasswordField
required
readonly={database.defaultDatabase}
disabled={database.defaultDatabase}
placeholder="eg: mydb"
placeholder="{$t('forms.eg')}: mydb"
id="defaultDatabase"
name="defaultDatabase"
bind:value={database.defaultDatabase}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="dbUser" class="text-base font-bold text-stone-100">User</label>
<label for="dbUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
id="dbUser"
name="dbUser"
value={database.dbUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
<label for="dbUserPassword" class="text-base font-bold text-stone-100"
>{$t('forms.password')}</label
>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField
id="dbUserPassword"
name="dbUserPassword"
@@ -43,22 +48,24 @@
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100">Root User</label>
<label for="rootUser" class="text-base font-bold text-stone-100">{$t('forms.root_user')}</label>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
id="rootUser"
name="rootUser"
value={database.rootUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="rootUserPassword" class="text-base font-bold text-stone-100">Root's Password</label>
<label for="rootUserPassword" class="text-base font-bold text-stone-100"
>{$t('forms.roots_password')}</label
>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField
id="rootUserPassword"
name="rootUserPassword"

View File

@@ -18,6 +18,7 @@
import { post } from '$lib/api';
import { getDomain } from '$lib/components/common';
import { toast } from '@zerodevx/svelte-toast';
import { t } from '$lib/translations';
const { id } = $page.params;
@@ -59,7 +60,7 @@
: window.location.hostname
: database.id
}:${isPublic ? database.publicPort : privatePort}/${databaseDefault}`
: 'Loading...');
: $t('forms.loading'));
}
async function changeSettings(name) {
@@ -110,20 +111,20 @@
<div class="mx-auto max-w-4xl px-6">
<form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex space-x-1 pb-5 font-bold">
<div class="title">General</div>
<div class="title">{$t('general')}</div>
{#if $session.isAdmin}
<button
type="submit"
class:bg-purple-600={!loading}
class:hover:bg-purple-500={!loading}
disabled={loading}>{loading ? 'Saving...' : 'Save'}</button
disabled={loading}>{loading ? $t('forms.saving') : $t('forms.save')}</button
>
{/if}
</div>
<div class="grid grid-flow-row gap-2 px-10">
<div class="grid grid-cols-2 items-center">
<label for="name" class="text-base font-bold text-stone-100">Name</label>
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
<input
readonly={!$session.isAdmin}
name="name"
@@ -133,7 +134,9 @@
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="destination" class="text-base font-bold text-stone-100">Destination</label>
<label for="destination" class="text-base font-bold text-stone-100"
>{$t('application.destination')}</label
>
{#if database.destinationDockerId}
<div class="no-underline">
<input
@@ -148,16 +151,17 @@
</div>
<div class="grid grid-cols-2 items-center">
<label for="version" class="text-base font-bold text-stone-100">Version</label>
<label for="version" class="text-base font-bold text-stone-100">{$t('forms.version')}</label
>
<input value={database.version} readonly disabled class="bg-transparent " />
</div>
</div>
<div class="grid grid-flow-row gap-2 px-10 pt-2">
<div class="grid grid-cols-2 items-center">
<label for="host" class="text-base font-bold text-stone-100">Host</label>
<label for="host" class="text-base font-bold text-stone-100">{$t('forms.host')}</label>
<CopyPasswordField
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField={false}
readonly
disabled
@@ -167,9 +171,10 @@
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="publicPort" class="text-base font-bold text-stone-100">Port</label>
<label for="publicPort" class="text-base font-bold text-stone-100">{$t('forms.port')}</label
>
<CopyPasswordField
placeholder="Generated automatically after set to public"
placeholder={$t('database.generated_automatically_after_set_to_public')}
id="publicPort"
readonly
disabled
@@ -191,10 +196,12 @@
<CouchDb {database} />
{/if}
<div class="grid grid-cols-2 items-center px-10 pb-8">
<label for="url" class="text-base font-bold text-stone-100">Connection String</label>
<label for="url" class="text-base font-bold text-stone-100"
>{$t('database.connection_string')}</label
>
<CopyPasswordField
textarea={true}
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField={false}
id="url"
name="url"
@@ -206,7 +213,7 @@
</div>
</form>
<div class="flex space-x-1 pb-5 font-bold">
<div class="title">Features</div>
<div class="title">{$t('application.features')}</div>
</div>
<div class="px-10 pb-10">
<div class="grid grid-cols-2 items-center">
@@ -214,8 +221,8 @@
loading={publicLoading}
bind:setting={isPublic}
on:click={() => changeSettings('isPublic')}
title="Set it public"
description="Your database will be reachable over the internet. <br>Take security seriously in this case!"
title={$t('database.set_public')}
description={$t('database.warning_database_public')}
disabled={!isRunning}
/>
</div>
@@ -224,8 +231,8 @@
<Setting
bind:setting={appendOnly}
on:click={() => changeSettings('appendOnly')}
title="Change append only mode"
description="Useful if you would like to restore redis data from a backup.<br><span class='font-bold text-white'>Database restart is required.</span>"
title={$t('database.change_append_only_mode')}
description={$t('database.warning_append_only')}
/>
</div>
{/if}

View File

@@ -3,6 +3,7 @@
export let isRunning;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations';
</script>
<div class="flex space-x-1 py-5 font-bold">
@@ -10,9 +11,9 @@
</div>
<div class="space-y-2 px-10">
<div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100">Root User</label>
<label for="rootUser" class="text-base font-bold text-stone-100">{$t('forms.root_user')}</label>
<CopyPasswordField
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
id="rootUser"
readonly
disabled
@@ -21,11 +22,13 @@
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="rootUserPassword" class="text-base font-bold text-stone-100">Root's Password</label>
<label for="rootUserPassword" class="text-base font-bold text-stone-100"
>{$t('forms.roots_password')}</label
>
<CopyPasswordField
disabled={!isRunning}
readonly={!isRunning}
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField={true}
id="rootUserPassword"
name="rootUserPassword"

View File

@@ -3,6 +3,7 @@
export let isRunning;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations';
</script>
<div class="flex space-x-1 py-5 font-bold">
@@ -10,34 +11,38 @@
</div>
<div class="space-y-2 px-10">
<div class="grid grid-cols-2 items-center">
<label for="defaultDatabase" class="text-base font-bold text-stone-100">Default Database</label>
<label for="defaultDatabase" class="text-base font-bold text-stone-100"
>{$t('database.default_database')}</label
>
<CopyPasswordField
required
readonly={database.defaultDatabase}
disabled={database.defaultDatabase}
placeholder="eg: mydb"
placeholder="{$t('forms.eg')}: mydb"
id="defaultDatabase"
name="defaultDatabase"
bind:value={database.defaultDatabase}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="dbUser" class="text-base font-bold text-stone-100">User</label>
<label for="dbUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
id="dbUser"
name="dbUser"
value={database.dbUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
<label for="dbUserPassword" class="text-base font-bold text-stone-100"
>{$t('forms.password')}</label
>
<CopyPasswordField
disabled={!isRunning}
readonly={!isRunning}
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField
id="dbUserPassword"
name="dbUserPassword"
@@ -46,22 +51,24 @@
<Explainer text="Could be changed while the database is running." />
</div>
<div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100">Root User</label>
<label for="rootUser" class="text-base font-bold text-stone-100">{$t('forms.root_user')}</label>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
id="rootUser"
name="rootUser"
value={database.rootUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="rootUserPassword" class="text-base font-bold text-stone-100">Root's Password</label>
<label for="rootUserPassword" class="text-base font-bold text-stone-100"
>{$t('forms.roots_password')}</label
>
<CopyPasswordField
disabled={!isRunning}
readonly={!isRunning}
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField
id="rootUserPassword"
name="rootUserPassword"

View File

@@ -3,6 +3,7 @@
export let isRunning;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations';
</script>
<div class="flex space-x-1 py-5 font-bold">
@@ -10,12 +11,14 @@
</div>
<div class="space-y-2 px-10">
<div class="grid grid-cols-2 items-center">
<label for="defaultDatabase" class="text-base font-bold text-stone-100">Default Database</label>
<label for="defaultDatabase" class="text-base font-bold text-stone-100"
>{$t('database.default_database')}</label
>
<CopyPasswordField
required
readonly={database.defaultDatabase}
disabled={database.defaultDatabase}
placeholder="eg: mydb"
placeholder="{$t('forms.eg')}: mydb"
id="defaultDatabase"
name="defaultDatabase"
bind:value={database.defaultDatabase}
@@ -29,28 +32,32 @@
disabled={!isRunning}
readonly={!isRunning}
placeholder="Generated automatically after start"
isPasswordField
id="rootUserPassword"
name="rootUserPassword"
bind:value={database.rootUserPassword}
/>
<Explainer text="Could be changed while the database is running." />
</div>
<div class="grid grid-cols-2 items-center">
<label for="dbUser" class="text-base font-bold text-stone-100">User</label>
<label for="dbUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
id="dbUser"
name="dbUser"
value={database.dbUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
<label for="dbUserPassword" class="text-base font-bold text-stone-100"
>{$t('forms.password')}</label
>
<CopyPasswordField
disabled={!isRunning}
readonly={!isRunning}
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField
id="dbUserPassword"
name="dbUserPassword"

View File

@@ -3,6 +3,7 @@
export let isRunning;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations';
</script>
<div class="flex space-x-1 py-5 font-bold">
@@ -10,11 +11,13 @@
</div>
<div class="space-y-2 px-10">
<div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
<label for="dbUserPassword" class="text-base font-bold text-stone-100"
>{$t('forms.password')}</label
>
<CopyPasswordField
disabled={!isRunning}
readonly={!isRunning}
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField
id="dbUserPassword"
name="dbUserPassword"

View File

@@ -63,6 +63,7 @@
import Loading from '$lib/components/Loading.svelte';
import { del, post } from '$lib/api';
import { goto } from '$app/navigation';
import { t } from '$lib/translations';
export let database;
export let isRunning;
@@ -83,7 +84,7 @@
}
}
async function stopDatabase() {
const sure = confirm(`Are you sure you would like to stop '${database.name}'?`);
const sure = confirm($t('database.confirm_stop', { name: database.name }));
if (sure) {
loading = true;
try {
@@ -113,13 +114,13 @@
{#if isRunning}
<button
on:click={stopDatabase}
title="Stop database"
title={$t('database.stop_database')}
type="submit"
disabled={!$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-red-500"
data-tooltip={$session.isAdmin
? 'Stop database'
: 'You do not have permission to stop the database.'}
? $t('database.stop_database')
: $t('database.permission_denied_stop_database')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -139,13 +140,13 @@
{:else}
<button
on:click={startDatabase}
title="Start database"
title={$t('database.start_database')}
type="submit"
disabled={!$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-green-500"
data-tooltip={$session.isAdmin
? 'Start database'
: 'You do not have permission to start the database.'}
? $t('database.start_database')
: $t('database.permission_denied_start_database')}
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
@@ -164,14 +165,14 @@
{/if}
<button
on:click={deleteDatabase}
title="Delete Database"
title={$t('database.delete_database')}
type="submit"
disabled={!$session.isAdmin}
class:hover:text-red-500={$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm"
data-tooltip={$session.isAdmin
? 'Delete Database'
: 'You do not have permission to delete a Database'}><DeleteIcon /></button
? $t('database.delete_database')
: $t('database.permission_denied_delete_database')}><DeleteIcon /></button
>
{/if}
</nav>

View File

@@ -34,6 +34,7 @@
import { errorNotification } from '$lib/form';
import { goto } from '$app/navigation';
import { post } from '$lib/api';
import { t } from '$lib/translations';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
@@ -52,12 +53,14 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Configure Destination</div>
<div class="mr-4 text-2xl tracking-tight">
{$t('application.configuration.configure_destination')}
</div>
</div>
<div class="flex justify-center">
{#if !destinations || destinations.length === 0}
<div class="flex-col">
<div class="pb-2">No configurable Destination found</div>
<div class="pb-2">{$t('application.configuration.no_configurable_destination')}</div>
<div class="flex justify-center">
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
<svg

View File

@@ -42,6 +42,7 @@
import Redis from '$lib/components/svg/databases/Redis.svelte';
import { goto } from '$app/navigation';
import { post } from '$lib/api';
import { t } from '$lib/translations';
async function handleSubmit(type) {
try {
await post(`/databases/${id}/configuration/type.json`, { type });
@@ -53,7 +54,7 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Select a Database type</div>
<div class="mr-4 text-2xl tracking-tight">{$t('database.select_database_type')}</div>
</div>
<div class="flex flex-wrap justify-center">

View File

@@ -31,6 +31,7 @@
import { enhance, errorNotification } from '$lib/form';
import { goto } from '$app/navigation';
import { post } from '$lib/api';
import { t } from '$lib/translations';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
@@ -47,7 +48,7 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Select a Database version</div>
<div class="mr-4 text-2xl tracking-tight">{$t('database.select_database_version')}</div>
</div>
<div class="flex flex-wrap justify-center">

View File

@@ -1,7 +1,7 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler, stopDatabase } from '$lib/database';
import { deleteProxy } from '$lib/haproxy';
import { stopTcpHttpProxy } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit';
export const del: RequestHandler = async (event) => {
@@ -12,7 +12,7 @@ export const del: RequestHandler = async (event) => {
const database = await db.getDatabase({ id, teamId });
if (database.destinationDockerId) {
const everStarted = await stopDatabase(database);
if (everStarted) await deleteProxy({ id });
if (everStarted) await stopTcpHttpProxy(database.destinationDocker, database.publicPort);
}
await db.removeDatabase({ id });
return { status: 200 };

View File

@@ -1,20 +1,16 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { generateDatabaseConfiguration, ErrorHandler } from '$lib/database';
import { generateDatabaseConfiguration, ErrorHandler, getFreePort } from '$lib/database';
import { startTcpProxy, stopTcpHttpProxy } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit';
import getPort, { portNumbers } from 'get-port';
export const post: RequestHandler = async (event) => {
const { status, body, teamId } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
const data = await db.prisma.setting.findFirst();
const { minPort, maxPort } = data;
const { isPublic, appendOnly = true } = await event.request.json();
const publicPort = await getPort({ port: portNumbers(minPort, maxPort) });
const publicPort = await getFreePort();
try {
await db.setDatabase({ id, isPublic, appendOnly });

View File

@@ -45,7 +45,15 @@ export const post: RequestHandler = async (event) => {
volumes: [volume],
ulimits,
labels,
restart: 'always'
restart: 'always',
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
}
},
networks: {

View File

@@ -9,6 +9,7 @@
import { post } from '$lib/api';
import { goto } from '$app/navigation';
import { session } from '$app/stores';
import { t } from '$lib/translations';
async function newDatabase() {
const { id } = await post('/databases/new', {});
@@ -27,7 +28,7 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Databases</div>
<div class="mr-4 text-2xl tracking-tight">{$t('index.databases')}</div>
<div on:click={newDatabase} class="add-icon cursor-pointer bg-purple-600 hover:bg-purple-500">
<svg
class="w-6"
@@ -48,7 +49,7 @@
<div class="flex flex-col flex-wrap justify-center">
{#if !databases || ownDatabases.length === 0}
<div class="flex-col">
<div class="text-center text-xl font-bold">No databases found</div>
<div class="text-center text-xl font-bold">{$t('database.no_databases_found')}</div>
</div>
{/if}
{#if ownDatabases.length > 0 || otherDatabases.length > 0}
@@ -78,7 +79,7 @@
{/if}
{#if !database.type}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Configuration missing
{$t('application.configuration.configuration_missing')}
</div>
{/if}
</div>

View File

@@ -2,6 +2,7 @@
export let app;
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { t } from '$lib/translations';
const { id } = $page.params;
let loading = true;
async function checkApp() {
@@ -58,20 +59,20 @@
<div class="box-selection hover:border-transparent hover:bg-coolgray-200">
<div class="truncate pb-2 text-center text-xl font-bold">{app.domain}</div>
{#if loading}
<div class="w-full text-center font-bold">Loading...</div>
<div class="w-full text-center font-bold">{$t('forms.loading')}</div>
{:else if app.foundByDomain}
<div class="w-full bg-coolgray-200 text-xs">
<span class="text-red-500">Domain</span> already used for
{@html $t('forms.already_used_for', { type: 'Domains' })}
<span class="text-red-500">{app.foundName}</span>
</div>
{:else if app.foundByRepository}
<div class="w-full bg-coolgray-200 text-xs">
<span class="text-red-500">Repository</span> already used for
{@html $t('forms.already_used_for', { type: 'Repository' })}
<span class="text-red-500">{app.foundName}</span>
</div>
{:else}
<button class="bg-green-600 hover:bg-green-500 w-full" on:click={addToCoolify}
>Add to Coolify</button
>{$t('destination.add_to_coolify')}</button
>
{/if}
</div>

View File

@@ -10,6 +10,7 @@
import { post } from '$lib/api';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { onMount } from 'svelte';
import { t } from '$lib/translations';
const { id } = $page.params;
let cannotDisable = settings.fqdn && destination.engine === '/var/run/docker.sock';
let loading = false;
@@ -85,7 +86,7 @@
async function stopProxy() {
try {
await post(`/destinations/${id}/stop.json`, { engine: destination.engine });
return toast.push('Coolify Proxy stopped!');
return toast.push($t('destination.coolify_proxy_stopped'));
} catch ({ error }) {
return errorNotification(error);
}
@@ -93,19 +94,17 @@
async function startProxy() {
try {
await post(`/destinations/${id}/start.json`, { engine: destination.engine });
return toast.push('Coolify Proxy started!');
return toast.push($t('destination.coolify_proxy_started'));
} catch ({ error }) {
return errorNotification(error);
}
}
async function forceRestartProxy() {
const sure = confirm(
'Are you sure you want to restart the proxy? Everything will be reconfigured in ~10 secs.'
);
const sure = confirm($t('destination.confirm_restart_proxy'));
if (sure) {
try {
restarting = true;
toast.push('Coolify Proxy restarting...');
toast.push($t('destination.coolify_proxy_restarting'));
await post(`/destinations/${id}/restart.json`, {
engine: destination.engine,
fqdn: settings.fqdn
@@ -123,7 +122,7 @@
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div class="flex space-x-1 pb-5">
<div class="title font-bold">Configuration</div>
<div class="title font-bold">{$t('forms.configuration')}</div>
{#if $session.isAdmin}
<button
type="submit"
@@ -131,13 +130,15 @@
class:bg-sky-600={!loading}
class:hover:bg-sky-500={!loading}
disabled={loading}
>{loading ? 'Saving...' : 'Save'}
>{loading ? $t('forms.saving') : $t('forms.save')}
</button>
<button
class={restarting ? '' : 'bg-red-600 hover:bg-red-500'}
disabled={restarting}
on:click|preventDefault={forceRestartProxy}
>{restarting ? 'Restarting... please wait...' : 'Force restart proxy'}</button
>{restarting
? $t('destination.restarting_please_wait')
: $t('destination.force_restart_proxy')}</button
>
{/if}
<!-- <button type="button" class="bg-coollabs hover:bg-coollabs-100" on:click={scanApps}
@@ -145,10 +146,10 @@
> -->
</div>
<div class="grid grid-cols-2 items-center px-10 ">
<label for="name" class="text-base font-bold text-stone-100">Name</label>
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
<input
name="name"
placeholder="name"
placeholder={$t('forms.name')}
disabled={!$session.isAdmin}
readonly={!$session.isAdmin}
bind:value={destination.name}
@@ -156,13 +157,13 @@
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="engine" class="text-base font-bold text-stone-100">Engine</label>
<label for="engine" class="text-base font-bold text-stone-100">{$t('forms.engine')}</label>
<CopyPasswordField
id="engine"
readonly
disabled
name="engine"
placeholder="eg: /var/run/docker.sock"
placeholder="{$t('forms.eg')}: /var/run/docker.sock"
value={destination.engine}
/>
</div>
@@ -171,13 +172,13 @@
<input name="remoteEngine" type="checkbox" bind:checked={payload.remoteEngine} />
</div> -->
<div class="grid grid-cols-2 items-center px-10">
<label for="network" class="text-base font-bold text-stone-100">Network</label>
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label>
<CopyPasswordField
id="network"
readonly
disabled
name="network"
placeholder="default: coolify"
placeholder="{$t('forms.default')}: coolify"
value={destination.network}
/>
</div>
@@ -188,7 +189,7 @@
disabled={cannotDisable}
bind:setting={destination.isCoolifyProxyUsed}
on:click={changeProxySetting}
title="Use Coolify Proxy?"
title={$t('destination.use_coolify_proxy')}
description={`This will install a proxy on the destination to allow you to access your applications and services without any manual configuration. Databases will have their own proxy. <br><br>${
cannotDisable
? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>'

View File

@@ -11,6 +11,7 @@
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { onMount } from 'svelte';
import { generateRemoteEngine } from '$lib/components/common';
import { t } from '$lib/translations';
const { id } = $page.params;
let cannotDisable = settings.fqdn && destination.engine === '/var/run/docker.sock';
// let scannedApps = [];
@@ -90,7 +91,7 @@
try {
const engine = generateRemoteEngine(destination);
await post(`/destinations/${id}/stop.json`, { engine });
return toast.push('Coolify Proxy stopped!');
return toast.push($t('destination.coolify_proxy_stopped'));
} catch ({ error }) {
return errorNotification(error);
}
@@ -99,19 +100,17 @@
try {
const engine = generateRemoteEngine(destination);
await post(`/destinations/${id}/start.json`, { engine });
return toast.push('Coolify Proxy started!');
return toast.push($t('destination.coolify_proxy_started'));
} catch ({ error }) {
return errorNotification(error);
}
}
async function forceRestartProxy() {
const sure = confirm(
'Are you sure you want to restart the proxy? Everything will be reconfigured in ~10 secs.'
);
const sure = confirm($t('destination.confirm_restart_proxy'));
if (sure) {
try {
restarting = true;
toast.push('Coolify Proxy restarting...');
toast.push($t('destination.coolify_proxy_restarting'));
await post(`/destinations/${id}/restart.json`, {
engine: destination.engine,
fqdn: settings.fqdn
@@ -127,7 +126,7 @@
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div class="flex space-x-1 pb-5">
<div class="title font-bold">Configuration</div>
<div class="title font-bold">{$t('forms.configuration')}</div>
{#if $session.isAdmin}
<button
type="submit"
@@ -135,13 +134,15 @@
class:bg-sky-600={!loading}
class:hover:bg-sky-500={!loading}
disabled={loading}
>{loading ? 'Saving...' : 'Save'}
>{loading ? $t('forms.saving') : $t('forms.save')}
</button>
<button
class={restarting ? '' : 'bg-red-600 hover:bg-red-500'}
disabled={restarting}
on:click|preventDefault={forceRestartProxy}
>{restarting ? 'Restarting... please wait...' : 'Force restart proxy'}</button
>{restarting
? $t('destination.restarting_please_wait')
: $t('destination.force_restart_proxy')}</button
>
{/if}
<!-- <button type="button" class="bg-coollabs hover:bg-coollabs-100" on:click={scanApps}
@@ -149,10 +150,10 @@
> -->
</div>
<div class="grid grid-cols-2 items-center px-10 ">
<label for="name" class="text-base font-bold text-stone-100">Name</label>
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
<input
name="name"
placeholder="name"
placeholder={$t('forms.name')}
disabled={!$session.isAdmin}
readonly={!$session.isAdmin}
bind:value={destination.name}
@@ -160,13 +161,13 @@
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="engine" class="text-base font-bold text-stone-100">Engine</label>
<label for="engine" class="text-base font-bold text-stone-100">{$t('forms.engine')}</label>
<CopyPasswordField
id="engine"
readonly
disabled
name="engine"
placeholder="eg: /var/run/docker.sock"
placeholder="{$t('forms.eg')}: /var/run/docker.sock"
value={destination.engine}
/>
</div>
@@ -175,13 +176,13 @@
<input name="remoteEngine" type="checkbox" bind:checked={payload.remoteEngine} />
</div> -->
<div class="grid grid-cols-2 items-center px-10">
<label for="network" class="text-base font-bold text-stone-100">Network</label>
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label>
<CopyPasswordField
id="network"
readonly
disabled
name="network"
placeholder="default: coolify"
placeholder="{$t('forms.default')}: coolify"
value={destination.network}
/>
</div>
@@ -190,7 +191,7 @@
disabled={cannotDisable}
bind:setting={destination.isCoolifyProxyUsed}
on:click={changeProxySetting}
title="Use Coolify Proxy?"
title={$t('destination.use_coolify_proxy')}
description={`This will install a proxy on the destination to allow you to access your applications and services without any manual configuration. Databases will have their own proxy. <br><br>${
cannotDisable
? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>'

View File

@@ -37,10 +37,11 @@
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import { del } from '$lib/api';
import { goto } from '$app/navigation';
import { t } from '$lib/translations';
export let destination;
async function deleteDestination(destination) {
const sure = confirm(`Are you sure you would like to delete '${destination.name}'?`);
const sure = confirm($t('application.confirm_to_delete', { name: destination.name }));
if (sure) {
try {
await del(`/destinations/${destination.id}.json`, { id: destination.id });
@@ -55,14 +56,14 @@
<nav class="nav-side">
<button
on:click={() => deleteDestination(destination)}
title="Delete Destination"
title={$t('destination.delete_destination')}
type="submit"
disabled={!$session.isAdmin}
class:hover:text-red-500={$session.isAdmin}
class="icons tooltip-bottom bg-transparent text-sm"
data-tooltip={$session.isAdmin
? 'Delete Destination'
: 'You do not have permission to delete this destination'}><DeleteIcon /></button
? $t('destination.delete_destination')
: $t('destination.permission_denied_delete_destination')}><DeleteIcon /></button
>
</nav>
<slot />

View File

@@ -8,7 +8,6 @@ import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
console.log(teamId);
const { id } = event.params;
try {
const destination = await db.getDestination({ id, teamId });

Some files were not shown because too many files have changed in this diff Show More