Compare commits

...

42 Commits
v3.12.33 ... v3

Author SHA1 Message Date
Andras Bacsai
b80a519b80 updates 2023-09-05 11:31:55 +02:00
Andras Bacsai
2fa7ffc931 updates 2023-09-05 11:25:16 +02:00
Andras Bacsai
4abec14a21 updates 2023-09-05 11:24:42 +02:00
Andras Bacsai
18d0623011 force volume prune 2023-08-16 15:27:10 +02:00
Andras Bacsai
aa634c78d1 fix seed js 2023-08-16 15:26:36 +02:00
Andras Bacsai
a2d4373104 cleanup volumes as well 2023-08-16 15:26:33 +02:00
Andras Bacsai
702e16d643 rename rollback to upgrade 2023-08-14 09:23:33 +02:00
Andras Bacsai
3b25c8f96b fix: docker compose env file 2023-08-12 00:10:14 +02:00
Andras Bacsai
1c8c567791 fix: env variables in compose deplyoments 2023-07-27 12:40:58 +02:00
Andras Bacsai
807a3c9d66 fix: n8n double mount
version++
2023-07-26 10:38:36 +02:00
Andras Bacsai
2abd7bd7bb copy to persisten storage 2023-07-25 12:38:36 +02:00
Andras Bacsai
343957ab8b update backups 2023-07-25 12:33:05 +02:00
Andras Bacsai
49261308f7 update webhook 2023-07-25 12:00:31 +02:00
Andras Bacsai
d037409237 updates 2023-07-25 11:40:27 +02:00
Andras Bacsai
338cbf62a1 fix: encrypt decrypt 2023-07-25 10:48:31 +02:00
Andras Bacsai
4c51bffc7b save db backup on seed 2023-07-20 21:41:47 +02:00
Andras Bacsai
fd98ba8812 auto update every hour 2023-07-20 16:37:24 +02:00
Andras Bacsai
930251e9c8 autoupdate fixed 2023-07-20 16:19:54 +02:00
Andras Bacsai
7cd441266a remove console log 2023-07-20 14:52:32 +02:00
Andras Bacsai
990fb8ec15 fix 2023-07-20 14:52:16 +02:00
Andras Bacsai
3fe982b2f4 Merge pull request #1052 from f-kawamura/bugfix-http-git-source
[Bug] Added support for HTTP source URLs in Git source
2023-07-20 13:48:34 +02:00
Andras Bacsai
9dd874e959 Merge pull request #1147 from martijnmichel/v3
Update serviceFields.ts
2023-07-20 13:46:52 +02:00
Andras Bacsai
b91368223b update plausible docs 2023-07-20 13:39:29 +02:00
Andras Bacsai
139670372b updates for templates 2023-07-20 13:29:03 +02:00
Andras Bacsai
1c0769ad75 update tags + only download on service view 2023-07-20 13:12:54 +02:00
Andras Bacsai
e6cbcf98cb fix: cleanup plausible 2023-07-20 13:06:19 +02:00
martijnmichel
64b0481055 Update serviceFields.ts 2023-07-20 08:19:51 +02:00
Andras Bacsai
ce15161926 Merge pull request #1144 from coollabsio/feature/implement-basic-auth-handling
fix: traefik config + ui + api
2023-07-18 15:37:57 +02:00
Andras Bacsai
4003d4d894 Merge pull request #1071 from pascal-klesse/feature/implement-basic-auth-handling
feat: Implement basic auth for applications
2023-07-18 15:37:35 +02:00
Andras Bacsai
6e011025a7 fix: traefik config + ui + api 2023-07-18 15:34:05 +02:00
Andras Bacsai
6c0544adb2 Merge branch 'v2' into feature/implement-basic-auth-handling 2023-07-18 14:48:02 +02:00
Andras Bacsai
8e4f7c9065 remove console.log 2023-07-18 14:44:46 +02:00
Andras Bacsai
e71f890b54 Merge pull request #1084 from Geczy/main
add trycatch
2023-07-18 14:44:32 +02:00
Andras Bacsai
4dc35dea97 Merge pull request #1119 from jenishngl/patch-1
Fixing the help link - source.svelte line 263
2023-07-18 14:40:33 +02:00
Andras Bacsai
b63dfb4bcd feat: backup databases 2023-07-18 14:36:54 +02:00
Andras Bacsai
b2ffd9183b fix: set connection string on publicity change 2023-07-18 13:01:10 +02:00
Jenish J
1fbcfcaf74 Merge branch 'v3' into patch-1 2023-07-17 20:15:27 +05:30
Jenish J
79c98657b1 Update source.svelte line 263 - Correcting the help link
Updated to the correct help link, as the old link was not pointing to the correct section in the docs.coollabs.io page
2023-06-28 20:45:03 +05:30
Geczy
d1be7e44af add trycatch 2023-05-25 13:39:57 -04:00
Pascal Klesse
eefc2a3d0e remove TODO 2023-05-15 09:33:03 +02:00
Pascal Klesse
d14ca724e9 feat: Implement basic auth for applications 2023-05-15 09:27:49 +02:00
f-kawamura
7b05aaffc3 fix: Added support for HTTP source URLs in Git source. Currently only support HTTPS 2023-04-24 16:12:47 +09:00
29 changed files with 5597 additions and 4974 deletions

View File

@@ -4,9 +4,8 @@ concurrency:
cancel-in-progress: true
on:
push:
branches-ignore:
- "main"
- "v4"
branches:
- "v3"
env:
REGISTRY: ghcr.io
IMAGE_NAME: "coollabsio/coolify"

11
.vscode/settings.json vendored
View File

@@ -18,5 +18,14 @@
"ts",
"json"
],
"i18n-ally.extract.autoDetect": true
"i18n-ally.extract.autoDetect": true,
"files.exclude": {
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.DS_Store": true,
"**/Thumbs.db": true
},
"hide-files.files": []
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
-- AlterTable
ALTER TABLE "Application" ADD COLUMN "basicAuthPw" TEXT;
ALTER TABLE "Application" ADD COLUMN "basicAuthUser" TEXT;
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_ApplicationSettings" (
"id" TEXT NOT NULL PRIMARY KEY,
"applicationId" TEXT NOT NULL,
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
"debug" BOOLEAN NOT NULL DEFAULT false,
"previews" BOOLEAN NOT NULL DEFAULT false,
"autodeploy" BOOLEAN NOT NULL DEFAULT true,
"isBot" BOOLEAN NOT NULL DEFAULT false,
"isPublicRepository" BOOLEAN NOT NULL DEFAULT false,
"isDBBranching" BOOLEAN NOT NULL DEFAULT false,
"isCustomSSL" BOOLEAN NOT NULL DEFAULT false,
"isHttp2" BOOLEAN NOT NULL DEFAULT false,
"basicAuth" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "ApplicationSettings_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_ApplicationSettings" ("applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isCustomSSL", "isDBBranching", "isHttp2", "isPublicRepository", "previews", "updatedAt") SELECT "applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isCustomSSL", "isDBBranching", "isHttp2", "isPublicRepository", "previews", "updatedAt" FROM "ApplicationSettings";
DROP TABLE "ApplicationSettings";
ALTER TABLE "new_ApplicationSettings" RENAME TO "ApplicationSettings";
CREATE UNIQUE INDEX "ApplicationSettings_applicationId_key" ON "ApplicationSettings"("applicationId");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -135,6 +135,8 @@ model Application {
dockerRegistryId String?
dockerRegistryImageName String?
simpleDockerfile String?
basicAuthUser String?
basicAuthPw String?
persistentStorage ApplicationPersistentStorage[]
secrets Secret[]
@@ -187,6 +189,7 @@ model ApplicationSettings {
isDBBranching Boolean @default(false)
isCustomSSL Boolean @default(false)
isHttp2 Boolean @default(false)
basicAuth Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
application Application @relation(fields: [applicationId], references: [id])

View File

@@ -95,8 +95,19 @@ async function main() {
}
async function reEncryptSecrets() {
const { execaCommand } = await import('execa');
const image = await execaCommand("docker inspect coolify --format '{{ .Config.Image }}'", {
shell: true
});
const version = image.stdout.split(':')[1] ?? null;
const date = new Date().getTime();
await execaCommand('env | grep COOLIFY > .env', { shell: true });
let backupfile = `/app/db/prod.db_${date}`;
if (version) {
backupfile = `/app/db/prod.db_${version}_${date}`;
}
await execaCommand('env | grep "^COOLIFY" | sort > .env', {
shell: true
});
const secretOld = process.env['COOLIFY_SECRET_KEY'];
let secretNew = process.env['COOLIFY_SECRET_KEY_BETTER'];
if (!secretNew) {
@@ -108,6 +119,8 @@ async function reEncryptSecrets() {
secretNew = newKey;
}
if (secretOld !== secretNew) {
console.log(`Backup database to ${backupfile}.`);
await execaCommand(`cp /app/db/prod.db ${backupfile}`, { shell: true });
console.log(
'Secrets (COOLIFY_SECRET_KEY & COOLIFY_SECRET_KEY_BETTER) are different, so re-encrypting everything...'
);
@@ -120,171 +133,209 @@ async function reEncryptSecrets() {
await execaCommand(`echo "COOLIFY_SECRET_KEY_OLD_${date}=${secretOld}" >> .env`, {
shell: true
});
console.log(`Backup database to /app/db/prod.db_${date}.`);
await execaCommand(`cp /app/db/prod.db /app/db/prod.db_${date}`, { shell: true });
const transactions = [];
const secrets = await prisma.secret.findMany();
if (secrets.length > 0) {
for (const secret of secrets) {
const value = decrypt(secret.value, secretOld);
const newValue = encrypt(value, secretNew);
transactions.push(
prisma.secret.update({
where: { id: secret.id },
data: { value: newValue }
})
);
try {
const value = decrypt(secret.value, secretOld);
const newValue = encrypt(value, secretNew);
transactions.push(
prisma.secret.update({
where: { id: secret.id },
data: { value: newValue }
})
);
} catch (e) {
console.log(e);
}
}
}
const serviceSecrets = await prisma.serviceSecret.findMany();
if (serviceSecrets.length > 0) {
for (const secret of serviceSecrets) {
const value = decrypt(secret.value, secretOld);
const newValue = encrypt(value, secretNew);
transactions.push(
prisma.serviceSecret.update({
where: { id: secret.id },
data: { value: newValue }
})
);
try {
const value = decrypt(secret.value, secretOld);
const newValue = encrypt(value, secretNew);
transactions.push(
prisma.serviceSecret.update({
where: { id: secret.id },
data: { value: newValue }
})
);
} catch (e) {
console.log(e);
}
}
}
const gitlabApps = await prisma.gitlabApp.findMany();
if (gitlabApps.length > 0) {
for (const gitlabApp of gitlabApps) {
const value = decrypt(gitlabApp.privateSshKey, secretOld);
const newValue = encrypt(value, secretNew);
const appSecret = decrypt(gitlabApp.appSecret, secretOld);
const newAppSecret = encrypt(appSecret, secretNew);
transactions.push(
prisma.gitlabApp.update({
where: { id: gitlabApp.id },
data: { privateSshKey: newValue, appSecret: newAppSecret }
})
);
try {
const value = decrypt(gitlabApp.privateSshKey, secretOld);
const newValue = encrypt(value, secretNew);
const appSecret = decrypt(gitlabApp.appSecret, secretOld);
const newAppSecret = encrypt(appSecret, secretNew);
transactions.push(
prisma.gitlabApp.update({
where: { id: gitlabApp.id },
data: { privateSshKey: newValue, appSecret: newAppSecret }
})
);
} catch (e) {
console.log(e);
}
}
}
const githubApps = await prisma.githubApp.findMany();
if (githubApps.length > 0) {
for (const githubApp of githubApps) {
const clientSecret = decrypt(githubApp.clientSecret, secretOld);
const newClientSecret = encrypt(clientSecret, secretNew);
const webhookSecret = decrypt(githubApp.webhookSecret, secretOld);
const newWebhookSecret = encrypt(webhookSecret, secretNew);
const privateKey = decrypt(githubApp.privateKey, secretOld);
const newPrivateKey = encrypt(privateKey, secretNew);
try {
const clientSecret = decrypt(githubApp.clientSecret, secretOld);
const newClientSecret = encrypt(clientSecret, secretNew);
const webhookSecret = decrypt(githubApp.webhookSecret, secretOld);
const newWebhookSecret = encrypt(webhookSecret, secretNew);
const privateKey = decrypt(githubApp.privateKey, secretOld);
const newPrivateKey = encrypt(privateKey, secretNew);
transactions.push(
prisma.githubApp.update({
where: { id: githubApp.id },
data: {
clientSecret: newClientSecret,
webhookSecret: newWebhookSecret,
privateKey: newPrivateKey
}
})
);
transactions.push(
prisma.githubApp.update({
where: { id: githubApp.id },
data: {
clientSecret: newClientSecret,
webhookSecret: newWebhookSecret,
privateKey: newPrivateKey
}
})
);
} catch (e) {
console.log(e);
}
}
}
const databases = await prisma.database.findMany();
if (databases.length > 0) {
for (const database of databases) {
const dbUserPassword = decrypt(database.dbUserPassword, secretOld);
const newDbUserPassword = encrypt(dbUserPassword, secretNew);
const rootUserPassword = decrypt(database.rootUserPassword, secretOld);
const newRootUserPassword = encrypt(rootUserPassword, secretNew);
transactions.push(
prisma.database.update({
where: { id: database.id },
data: {
dbUserPassword: newDbUserPassword,
rootUserPassword: newRootUserPassword
}
})
);
try {
const dbUserPassword = decrypt(database.dbUserPassword, secretOld);
const newDbUserPassword = encrypt(dbUserPassword, secretNew);
const rootUserPassword = decrypt(database.rootUserPassword, secretOld);
const newRootUserPassword = encrypt(rootUserPassword, secretNew);
transactions.push(
prisma.database.update({
where: { id: database.id },
data: {
dbUserPassword: newDbUserPassword,
rootUserPassword: newRootUserPassword
}
})
);
} catch (e) {
console.log(e);
}
}
}
const databaseSecrets = await prisma.databaseSecret.findMany();
if (databaseSecrets.length > 0) {
for (const databaseSecret of databaseSecrets) {
const value = decrypt(databaseSecret.value, secretOld);
const newValue = encrypt(value, secretNew);
transactions.push(
prisma.databaseSecret.update({
where: { id: databaseSecret.id },
data: { value: newValue }
})
);
try {
const value = decrypt(databaseSecret.value, secretOld);
const newValue = encrypt(value, secretNew);
transactions.push(
prisma.databaseSecret.update({
where: { id: databaseSecret.id },
data: { value: newValue }
})
);
} catch (e) {
console.log(e);
}
}
}
const wordpresses = await prisma.wordpress.findMany();
if (wordpresses.length > 0) {
for (const wordpress of wordpresses) {
const value = decrypt(wordpress.ftpHostKey, secretOld);
const newValue = encrypt(value, secretNew);
const ftpHostKeyPrivate = decrypt(wordpress.ftpHostKeyPrivate, secretOld);
const newFtpHostKeyPrivate = encrypt(ftpHostKeyPrivate, secretNew);
let newFtpPassword = undefined;
if (wordpress.ftpPassword != null) {
const ftpPassword = decrypt(wordpress.ftpPassword, secretOld);
newFtpPassword = encrypt(ftpPassword, secretNew);
}
try {
const value = decrypt(wordpress.ftpHostKey, secretOld);
const newValue = encrypt(value, secretNew);
const ftpHostKeyPrivate = decrypt(wordpress.ftpHostKeyPrivate, secretOld);
const newFtpHostKeyPrivate = encrypt(ftpHostKeyPrivate, secretNew);
let newFtpPassword = undefined;
if (wordpress.ftpPassword != null) {
const ftpPassword = decrypt(wordpress.ftpPassword, secretOld);
newFtpPassword = encrypt(ftpPassword, secretNew);
}
transactions.push(
prisma.wordpress.update({
where: { id: wordpress.id },
data: {
ftpHostKey: newValue,
ftpHostKeyPrivate: newFtpHostKeyPrivate,
ftpPassword: newFtpPassword
}
})
);
transactions.push(
prisma.wordpress.update({
where: { id: wordpress.id },
data: {
ftpHostKey: newValue,
ftpHostKeyPrivate: newFtpHostKeyPrivate,
ftpPassword: newFtpPassword
}
})
);
} catch (e) {
console.log(e);
}
}
}
const sshKeys = await prisma.sshKey.findMany();
if (sshKeys.length > 0) {
for (const key of sshKeys) {
const value = decrypt(key.privateKey, secretOld);
const newValue = encrypt(value, secretNew);
transactions.push(
prisma.sshKey.update({
where: { id: key.id },
data: {
privateKey: newValue
}
})
);
try {
const value = decrypt(key.privateKey, secretOld);
const newValue = encrypt(value, secretNew);
transactions.push(
prisma.sshKey.update({
where: { id: key.id },
data: {
privateKey: newValue
}
})
);
} catch (e) {
console.log(e);
}
}
}
const dockerRegistries = await prisma.dockerRegistry.findMany();
if (dockerRegistries.length > 0) {
for (const registry of dockerRegistries) {
const value = decrypt(registry.password, secretOld);
const newValue = encrypt(value, secretNew);
transactions.push(
prisma.dockerRegistry.update({
where: { id: registry.id },
data: {
password: newValue
}
})
);
try {
const value = decrypt(registry.password, secretOld);
const newValue = encrypt(value, secretNew);
transactions.push(
prisma.dockerRegistry.update({
where: { id: registry.id },
data: {
password: newValue
}
})
);
} catch (e) {
console.log(e);
}
}
}
const certificates = await prisma.certificate.findMany();
if (certificates.length > 0) {
for (const certificate of certificates) {
const value = decrypt(certificate.key, secretOld);
const newValue = encrypt(value, secretNew);
transactions.push(
prisma.certificate.update({
where: { id: certificate.id },
data: {
key: newValue
}
})
);
try {
const value = decrypt(certificate.key, secretOld);
const newValue = encrypt(value, secretNew);
transactions.push(
prisma.certificate.update({
where: { id: certificate.id },
data: {
key: newValue
}
})
);
} catch (e) {
console.log(e);
}
}
}
await prisma.$transaction(transactions);
@@ -306,18 +357,16 @@ const encrypt = (text, secret) => {
};
const decrypt = (hashString, secret) => {
if (hashString && secret) {
try {
const hash = JSON.parse(hashString);
const decipher = crypto.createDecipheriv(algorithm, secret, Buffer.from(hash.iv, 'hex'));
const decrpyted = Buffer.concat([
decipher.update(Buffer.from(hash.content, 'hex')),
decipher.final()
]);
return decrpyted.toString();
} catch (error) {
console.log({ decryptionError: error.message });
return hashString;
const hash = JSON.parse(hashString);
const decipher = crypto.createDecipheriv(algorithm, secret, Buffer.from(hash.iv, 'hex'));
const decrpyted = Buffer.concat([
decipher.update(Buffer.from(hash.content, 'hex')),
decipher.final()
]);
if (/<2F>/.test(decrpyted.toString())) {
throw new Error('Invalid secret. Skipping...');
}
return decrpyted.toString();
}
};

View File

@@ -1,14 +1,19 @@
import Fastify from 'fastify';
import cors from '@fastify/cors';
import serve from '@fastify/static';
import env from '@fastify/env';
import cookie from '@fastify/cookie';
import multipart from '@fastify/multipart';
import path, { join } from 'path';
import autoLoad from '@fastify/autoload';
import cookie from '@fastify/cookie';
import cors from '@fastify/cors';
import env from '@fastify/env';
import multipart from '@fastify/multipart';
import serve from '@fastify/static';
import Fastify from 'fastify';
import socketIO from 'fastify-socket.io';
import path, { join } from 'path';
import socketIOServer from './realtime';
import Graceful from '@ladjs/graceful';
import { compareVersions } from 'compare-versions';
import fs from 'fs/promises';
import yaml from 'js-yaml';
import { migrateApplicationPersistentStorage, migrateServicesToNewTemplate } from './lib';
import {
cleanupDockerStorage,
createRemoteEngineConfiguration,
@@ -22,14 +27,9 @@ import {
startTraefikTCPProxy,
version
} from './lib/common';
import { scheduler } from './lib/scheduler';
import { compareVersions } from 'compare-versions';
import Graceful from '@ladjs/graceful';
import yaml from 'js-yaml';
import fs from 'fs/promises';
import { verifyRemoteDockerEngineFn } from './routes/api/v1/destinations/handlers';
import { checkContainer } from './lib/docker';
import { migrateApplicationPersistentStorage, migrateServicesToNewTemplate } from './lib';
import { scheduler } from './lib/scheduler';
import { verifyRemoteDockerEngineFn } from './routes/api/v1/destinations/handlers';
import { refreshTags, refreshTemplates } from './routes/api/v1/handlers';
declare module 'fastify' {
interface FastifyInstance {
@@ -167,7 +167,7 @@ const host = '0.0.0.0';
// autoUpdater
setInterval(async () => {
await autoUpdater();
}, 60000 * 15);
}, 60000 * 60);
// cleanupStorage
setInterval(async () => {
@@ -209,7 +209,9 @@ const host = '0.0.0.0';
getTagsTemplates(),
getArch(),
getIPAddress(),
configureRemoteDockers()
configureRemoteDockers(),
refreshTemplates(),
refreshTags()
// cleanupStuckedContainers()
]);
} catch (error) {
@@ -402,16 +404,21 @@ async function autoUpdater() {
if (!isDev) {
const { isAutoUpdateEnabled } = await prisma.setting.findFirst();
if (isAutoUpdateEnabled) {
await executeCommand({
command: `docker pull ghcr.io/coollabsio/coolify:${latestVersion}`
});
await executeCommand({ shell: true, command: `env | grep '^COOLIFY' > .env` });
let image = `ghcr.io/coollabsio/coolify:${latestVersion}`;
try {
await executeCommand({ command: `docker pull ${image}` });
} catch (error) {
image = `coollabsio/coolify:${latestVersion}`;
await executeCommand({ command: `docker pull ${image}` });
}
await executeCommand({ shell: true, command: `ls .env || env | grep "^COOLIFY" | sort > .env` });
await executeCommand({
command: `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env`
});
await executeCommand({
shell: true,
command: `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db ghcr.io/coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"`
command: `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db ${image} /bin/sh -c "env | grep "^COOLIFY" | sort > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"`
});
}
} else {
@@ -548,7 +555,11 @@ async function copySSLCertificates() {
} catch (error) {
console.log(error);
} finally {
await executeCommand({ command: `find /tmp/ -maxdepth 1 -type f -name '*-*.pem' -delete` });
try {
await executeCommand({ command: `find /tmp/ -maxdepth 1 -type f -name '*-*.pem' -delete` });
} catch (e) {
console.log(e);
}
}
}

View File

@@ -19,7 +19,9 @@ export default async function (data) {
dockerComposeConfiguration,
dockerComposeFileLocation
} = data;
const fileYaml = `${workdir}${baseDirectory}${dockerComposeFileLocation}`;
const baseDir = `${workdir}${baseDirectory}`;
const envFile = `${baseDir}/.env`;
const fileYaml = `${baseDir}${dockerComposeFileLocation}`;
const dockerComposeRaw = await fs.readFile(fileYaml, 'utf8');
const dockerComposeYaml = yaml.load(dockerComposeRaw);
if (!dockerComposeYaml.services) {
@@ -31,7 +33,7 @@ export default async function (data) {
envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId, false, null)];
buildEnvs = [...buildEnvs, ...generateSecrets(secrets, pullmergeRequestId, true, null, true)];
}
await fs.writeFile(envFile, envs.join('\n'));
const composeVolumes = [];
if (volumes.length > 0) {
for (const volume of volumes) {
@@ -50,32 +52,38 @@ export default async function (data) {
if (value['env_file']) {
delete value['env_file'];
}
value['env_file'] = [envFile];
let environment = typeof value['environment'] === 'undefined' ? [] : value['environment'];
console.log({ key, environment });
if (Object.keys(environment).length > 0) {
environment = Object.entries(environment).map(([key, value]) => `${key}=${value}`);
}
value['environment'] = [...environment, ...envs];
// let environment = typeof value['environment'] === 'undefined' ? [] : value['environment'];
// let finalEnvs = [...envs];
// if (Object.keys(environment).length > 0) {
// for (const arg of Object.keys(environment)) {
// const [key, _] = arg.split('=');
// if (finalEnvs.filter((env) => env.startsWith(key)).length === 0) {
// finalEnvs.push(arg);
// }
// }
// }
// value['environment'] = [...finalEnvs];
let build = typeof value['build'] === 'undefined' ? [] : value['build'];
if (typeof build === 'string') {
build = { context: build };
}
const buildArgs = typeof build['args'] === 'undefined' ? [] : build['args'];
let finalArgs = [...buildEnvs];
let finalBuildArgs = [...buildEnvs];
if (Object.keys(buildArgs).length > 0) {
for (const arg of Object.keys(buildArgs)) {
const [key, _] = arg.split('=');
if (finalArgs.filter((env) => env.startsWith(key)).length === 0) {
finalArgs.push(arg);
if (finalBuildArgs.filter((env) => env.startsWith(key)).length === 0) {
finalBuildArgs.push(arg);
}
}
}
if (build.length > 0 || buildArgs.length > 0) {
value['build'] = {
...build,
args: finalArgs
args: finalBuildArgs
};
}
@@ -122,7 +130,6 @@ export default async function (data) {
.replace(/^\./, `~`)
.replace(/^\.\./, '~')
.replace(/^\$PWD/, '~');
console.log({ source });
} else {
if (!target) {
target = source;

View File

@@ -1,6 +1,5 @@
import { exec } from 'node:child_process';
import util from 'util';
import fs from 'fs/promises';
import fsNormal from 'fs';
import yaml from 'js-yaml';
import forge from 'node-forge';
import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator';
@@ -17,8 +16,9 @@ import { day } from './dayjs';
import { saveBuildLog } from './buildPacks/common';
import { scheduler } from './scheduler';
import type { ExecaChildProcess } from 'execa';
import { FastifyReply } from 'fastify';
export const version = '3.12.33';
export const version = '3.12.39';
export const isDev = process.env.NODE_ENV === 'development';
export const proxyPort = process.env.COOLIFY_PROXY_PORT;
export const proxySecurePort = process.env.COOLIFY_PROXY_SECURE_PORT;
@@ -1762,7 +1762,7 @@ export function convertTolOldVolumeNames(type) {
}
}
export async function cleanupDockerStorage(dockerId) {
export async function cleanupDockerStorage(dockerId, volumes = false) {
// Cleanup images that are not used by any container
try {
await executeCommand({ dockerId, command: `docker image prune -af` });
@@ -1780,6 +1780,11 @@ export async function cleanupDockerStorage(dockerId) {
try {
await executeCommand({ dockerId, command: `docker builder prune -af` });
} catch (error) { }
if (volumes) {
try {
await executeCommand({ dockerId, command: `docker volume prune -af` });
} catch (error) { }
}
}
export function persistentVolumes(id, persistentStorage, config) {
@@ -1942,3 +1947,49 @@ export function generateSecrets(
}
return envs;
}
export async function backupDatabaseNow(database, reply) {
const backupFolder = '/tmp'
const fileName = `${database.id}-${new Date().getTime()}.gz`
const backupFileName = `${backupFolder}/${fileName}`
const backupStorageFilename = `/app/backups/${fileName}`
let command = null
switch (database?.type) {
case 'postgresql':
command = `docker exec ${database.id} sh -c "PGPASSWORD=${database.rootUserPassword} pg_dumpall -U postgres | gzip > ${backupFileName}"`
break;
case 'mongodb':
command = `docker exec ${database.id} sh -c "mongodump --archive=${backupFileName} --gzip --username=${database.rootUser} --password=${database.rootUserPassword}"`
break;
case 'mysql':
command = `docker exec ${database.id} sh -c "mysqldump --all-databases --single-transaction --quick --lock-tables=false --user=${database.rootUser} --password=${database.rootUserPassword} | gzip > ${backupFileName}"`
break;
case 'mariadb':
command = `docker exec ${database.id} sh -c "mysqldump --all-databases --single-transaction --quick --lock-tables=false --user=${database.rootUser} --password=${database.rootUserPassword} | gzip > ${backupFileName}"`
break;
case 'couchdb':
command = `docker exec ${database.id} sh -c "tar -czvf ${backupFileName} /bitnami/couchdb/data"`
break;
default:
return;
}
await executeCommand({
dockerId: database.destinationDockerId,
command,
});
const copyCommand = `docker cp ${database.id}:${backupFileName} ${backupFileName}`
await executeCommand({
dockerId: database.destinationDockerId,
command: copyCommand
});
await executeCommand({
dockerId: database.destinationDockerId,
command: `docker cp ${database.id}:${backupFileName} /app/backups/`
});
const stream = fsNormal.createReadStream(backupFileName);
reply.header('Content-Type', 'application/octet-stream');
reply.header('Content-Disposition', `attachment; filename=${fileName}`);
reply.header('Content-Length', fsNormal.statSync(backupFileName).size);
reply.header('Content-Transfer-Encoding', 'binary');
return reply.send(stream)
}

View File

@@ -624,7 +624,7 @@ export const glitchTip = [{
isEncrypted: false
},
{
name: 'emailSmtpUseSsl',
name: 'emailSmtpUseTls',
isEditable: true,
isLowerCase: false,
isNumber: false,

View File

@@ -398,7 +398,9 @@ export async function saveApplication(
dockerComposeFileLocation,
dockerComposeConfiguration,
simpleDockerfile,
dockerRegistryImageName
dockerRegistryImageName,
basicAuthPw,
basicAuthUser,
} = request.body;
if (port) port = Number(port);
if (exposePort) {
@@ -453,6 +455,8 @@ export async function saveApplication(
dockerComposeConfiguration,
simpleDockerfile,
dockerRegistryImageName,
basicAuthPw,
basicAuthUser,
...defaultConfiguration,
connectedDatabase: { update: { hostedDatabaseDBName: baseDatabaseBranch } }
}
@@ -476,6 +480,8 @@ export async function saveApplication(
dockerComposeFileLocation,
dockerComposeConfiguration,
simpleDockerfile,
basicAuthPw,
basicAuthUser,
dockerRegistryImageName,
...defaultConfiguration
}
@@ -499,12 +505,11 @@ export async function saveApplicationSettings(
previews,
dualCerts,
autodeploy,
branch,
projectId,
isBot,
isDBBranching,
isCustomSSL,
isHttp2
isHttp2,
basicAuth,
} = request.body;
await prisma.application.update({
where: { id },
@@ -519,7 +524,8 @@ export async function saveApplicationSettings(
isBot,
isDBBranching,
isCustomSSL,
isHttp2
isHttp2,
basicAuth,
}
}
},

View File

@@ -28,6 +28,8 @@ export interface SaveApplication extends OnlyId {
dockerComposeConfiguration: string;
simpleDockerfile: string;
dockerRegistryImageName: string;
basicAuthPw: string;
basicAuthUser: string;
};
}
export interface SaveApplicationSettings extends OnlyId {
@@ -43,6 +45,7 @@ export interface SaveApplicationSettings extends OnlyId {
isDBBranching: boolean;
isCustomSSL: boolean;
isHttp2: boolean;
basicAuth: boolean;
};
}
export interface DeleteApplication extends OnlyId {

View File

@@ -5,6 +5,7 @@ import yaml from 'js-yaml';
import fs from 'fs/promises';
import {
ComposeFile,
backupDatabaseNow,
createDirectories,
decrypt,
defaultComposeConfiguration,
@@ -351,6 +352,21 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
return errorHandler({ status, message });
}
}
export async function backupDatabase(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
try {
const teamId = request.user.teamId;
const { id } = request.params;
const database = await prisma.database.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, settings: true }
});
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
return await backupDatabaseNow(database, reply);
} catch ({ status, message }) {
return errorHandler({ status, message });
}
}
export async function stopDatabase(request: FastifyRequest<OnlyId>) {
try {
const teamId = request.user.teamId;

View File

@@ -1,5 +1,5 @@
import { FastifyPluginAsync } from 'fastify';
import { cleanupUnconfiguredDatabases, deleteDatabase, deleteDatabaseSecret, getDatabase, getDatabaseLogs, getDatabaseSecrets, getDatabaseStatus, getDatabaseTypes, getDatabaseUsage, getVersions, listDatabases, newDatabase, saveDatabase, saveDatabaseDestination, saveDatabaseSecret, saveDatabaseSettings, saveDatabaseType, saveVersion, startDatabase, stopDatabase } from './handlers';
import { backupDatabase, cleanupUnconfiguredDatabases, deleteDatabase, deleteDatabaseSecret, getDatabase, getDatabaseLogs, getDatabaseSecrets, getDatabaseStatus, getDatabaseTypes, getDatabaseUsage, getVersions, listDatabases, newDatabase, saveDatabase, saveDatabaseDestination, saveDatabaseSecret, saveDatabaseSettings, saveDatabaseType, saveVersion, startDatabase, stopDatabase } from './handlers';
import type { OnlyId } from '../../../../types';
@@ -39,6 +39,7 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.post<OnlyId>('/:id/start', async (request) => await startDatabase(request));
fastify.post<OnlyId>('/:id/stop', async (request) => await stopDatabase(request));
fastify.post<OnlyId>('/:id/backup', async (request, reply) => await backupDatabase(request, reply));
};
export default root;

View File

@@ -19,8 +19,7 @@ import type { FastifyReply, FastifyRequest } from 'fastify';
import type { Login, Update } from '.';
import type { GetCurrentUser } from './types';
export async function hashPassword(password: string): Promise<string> {
const saltRounds = 15;
export async function hashPassword(password: string, saltRounds = 15): Promise<string> {
return bcrypt.hash(password, saltRounds);
}
@@ -58,7 +57,7 @@ export async function cleanupManually(request: FastifyRequest) {
const destination = await prisma.destinationDocker.findUnique({
where: { id: serverId }
});
await cleanupDockerStorage(destination.id);
await cleanupDockerStorage(destination.id, true);
return {};
} catch ({ status, message }) {
return errorHandler({ status, message });
@@ -77,7 +76,7 @@ export async function refreshTags() {
tags = JSON.parse(tags).concat(JSON.parse(testTags));
}
}
} catch (error) {}
} catch (error) { }
await fs.writeFile('./tags.json', tags);
} else {
const tags = await got.get('https://get.coollabs.io/coolify/service-tags.json').text();
@@ -102,7 +101,7 @@ export async function refreshTemplates() {
if (await fs.stat('./testTemplate.yaml')) {
templates = templates + (await fs.readFile('./testTemplate.yaml', 'utf8'));
}
} catch (error) {}
} catch (error) { }
const response = await fs.readFile('./devTemplates.yaml', 'utf8');
await fs.writeFile('./templates.json', JSON.stringify(yaml.load(response)));
} else {
@@ -163,13 +162,13 @@ export async function update(request: FastifyRequest<Update>) {
await executeCommand({ command: `docker pull ${image}` });
}
await executeCommand({ shell: true, command: `ls .env || env | grep COOLIFY > .env` });
await executeCommand({ shell: true, command: `ls .env || env | grep "^COOLIFY" | sort > .env` });
await executeCommand({
command: `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env`
});
await executeCommand({
shell: true,
command: `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db ${image} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"`
command: `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db ${image} /bin/sh -c "env | grep "^COOLIFY" | sort > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"`
});
return {};
} else {

View File

@@ -50,6 +50,7 @@ import type {
SetWordpressSettings
} from './types';
import type { OnlyId } from '../../../../types';
import { refreshTags, refreshTemplates } from '../handlers';
export async function listServices(request: FastifyRequest) {
try {
@@ -476,7 +477,7 @@ export async function saveServiceType(
const [volumeName, path] = volume.split(':');
if (!volumeName.startsWith('/')) {
const found = await prisma.servicePersistentStorage.findFirst({
where: { volumeName, serviceId: id }
where: { volumeName, serviceId: id, path }
});
if (!found) {
await prisma.servicePersistentStorage.create({
@@ -985,11 +986,22 @@ export async function cleanupPlausibleLogs(request: FastifyRequest<OnlyId>, repl
const teamId = request.user.teamId;
const { destinationDockerId, destinationDocker } = await getServiceFromDB({ id, teamId });
if (destinationDockerId) {
await executeCommand({
const logTables = await executeCommand({
dockerId: destinationDocker.id,
command: `docker exec ${id}-clickhouse /usr/bin/clickhouse-client -q \\"SELECT name FROM system.tables WHERE name LIKE '%log%';\\"| xargs -I{} /usr/bin/clickhouse-client -q \"TRUNCATE TABLE system.{};\"`,
shell: true
command: `docker exec ${id}-clickhouse clickhouse-client -q "SELECT name FROM system.tables;"`,
shell: false
});
if (logTables.stdout !== '') {
const tables = logTables.stdout.split('\n').filter((t) => t.includes('_log'));
for (const table of tables) {
console.log(`Truncating table ${table}`)
await executeCommand({
dockerId: destinationDocker.id,
command: `docker exec ${id}-clickhouse clickhouse-client -q "TRUNCATE TABLE system.${table};"`,
shell: false
});
}
}
return await reply.code(201).send();
}
throw { status: 500, message: 'Could cleanup logs.' };
@@ -1105,17 +1117,14 @@ export async function activateWordpressFtp(
shell: true
});
}
} catch (error) {}
} catch (error) { }
const volumes = [
`${id}-wordpress-data:/home/${ftpUser}/wordpress`,
`${
isDev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
`${isDev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
}/${id}.ed25519:/etc/ssh/ssh_host_ed25519_key`,
`${
isDev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
`${isDev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
}/${id}.rsa:/etc/ssh/ssh_host_rsa_key`,
`${
isDev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
`${isDev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
}/${id}.sh:/etc/sftp.d/chmod.sh`
];
@@ -1185,6 +1194,6 @@ export async function activateWordpressFtp(
await executeCommand({
command: `rm -fr ${hostkeyDir}/${id}-docker-compose.yml ${hostkeyDir}/${id}.ed25519 ${hostkeyDir}/${id}.ed25519.pub ${hostkeyDir}/${id}.rsa ${hostkeyDir}/${id}.rsa.pub ${hostkeyDir}/${id}.sh`
});
} catch (error) {}
} catch (error) { }
}
}

View File

@@ -1,8 +1,9 @@
import { FastifyRequest } from 'fastify';
import { errorHandler, getDomain, isDev, prisma, executeCommand } from '../../../lib/common';
import { errorHandler, executeCommand, getDomain, isDev, prisma } from '../../../lib/common';
import { getTemplates } from '../../../lib/services';
import { OnlyId } from '../../../types';
import { parseAndFindServiceTemplates } from '../../api/v1/services/handlers';
import { hashPassword } from '../../api/v1/handlers';
function generateServices(serviceId, containerId, port, isHttp2 = false, isHttps = false) {
if (isHttp2) {
@@ -39,7 +40,7 @@ function generateServices(serviceId, containerId, port, isHttp2 = false, isHttps
}
};
}
function generateRouters(
async function generateRouters({
serviceId,
domain,
nakedDomain,
@@ -48,20 +49,22 @@ function generateRouters(
isWWW,
isDualCerts,
isCustomSSL,
isHttp2 = false
) {
let rule = `Host(\`${nakedDomain}\`)${pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''}`;
let ruleWWW = `Host(\`www.${nakedDomain}\`)${
pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''
}`;
let http: any = {
isHttp2 = false,
httpBasicAuth = null,
}) {
const rule = `Host(\`${nakedDomain}\`)${pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''}`;
const ruleWWW = `Host(\`www.${nakedDomain}\`)${pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''
}`;
const http: any = {
entrypoints: ['web'],
rule,
service: `${serviceId}`,
priority: 2,
middlewares: []
};
let https: any = {
const https: any = {
entrypoints: ['websecure'],
rule,
service: `${serviceId}`,
@@ -71,14 +74,14 @@ function generateRouters(
},
middlewares: []
};
let httpWWW: any = {
const httpWWW: any = {
entrypoints: ['web'],
rule: ruleWWW,
service: `${serviceId}`,
priority: 2,
middlewares: []
};
let httpsWWW: any = {
const httpsWWW: any = {
entrypoints: ['websecure'],
rule: ruleWWW,
service: `${serviceId}`,
@@ -97,6 +100,10 @@ function generateRouters(
httpsWWW.middlewares.push('redirect-to-non-www');
delete https.tls;
delete httpsWWW.tls;
if (httpBasicAuth) {
http.middlewares.push(`${serviceId}-${pathPrefix}-basic-auth`);
}
}
// 3. http + www only
@@ -108,6 +115,10 @@ function generateRouters(
https.middlewares.push('redirect-to-www');
delete https.tls;
delete httpsWWW.tls;
if (httpBasicAuth) {
httpWWW.middlewares.push(`${serviceId}-${pathPrefix}-basic-auth`);
}
}
// 5. https + non-www only
if (isHttps && !isWWW) {
@@ -136,6 +147,10 @@ function generateRouters(
};
}
}
if (httpBasicAuth) {
https.middlewares.push(`${serviceId}-${pathPrefix}-basic-auth`);
}
}
// 6. https + www only
if (isHttps && isWWW) {
@@ -145,6 +160,11 @@ function generateRouters(
http.middlewares.push('redirect-to-www');
https.middlewares.push('redirect-to-www');
}
if (httpBasicAuth) {
httpsWWW.middlewares.push(`${serviceId}-${pathPrefix}-basic-auth`);
}
if (isCustomSSL) {
if (isDualCerts) {
https.tls = true;
@@ -166,23 +186,23 @@ function generateRouters(
}
}
if (isHttp2) {
let http2 = {
const http2 = {
...http,
service: `${serviceId}-http2`,
rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)`
};
let http2WWW = {
const http2WWW = {
...httpWWW,
service: `${serviceId}-http2`,
rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)`
};
let https2 = {
const https2 = {
...https,
service: `${serviceId}-http2`,
rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)`
};
let https2WWW = {
const https2WWW = {
...httpsWWW,
service: `${serviceId}-http2`,
rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)`
@@ -198,14 +218,17 @@ function generateRouters(
[`${serviceId}-${pathPrefix}-secure-www-http2`]: { ...https2WWW }
};
}
return {
const result = {
[`${serviceId}-${pathPrefix}`]: { ...http },
[`${serviceId}-${pathPrefix}-secure`]: { ...https },
[`${serviceId}-${pathPrefix}-www`]: { ...httpWWW },
[`${serviceId}-${pathPrefix}-secure-www`]: { ...httpsWWW }
};
return result;
}
export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote: boolean = false) {
export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote = false) {
const traefik = {
tls: {
certificates: []
@@ -298,7 +321,7 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
});
}
let parsedCertificates = [];
const parsedCertificates = [];
for (const certificate of certificates) {
parsedCertificates.push({
certFile: `${sslpath}/${certificate.id}-cert.pem`,
@@ -369,7 +392,10 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
dockerComposeConfiguration,
destinationDocker,
destinationDockerId,
settings
settings,
basicAuthUser,
basicAuthPw,
settings: { basicAuth: isBasicAuthEnabled }
} = application;
if (!destinationDockerId) {
continue;
@@ -382,6 +408,14 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
) {
continue;
}
let httpBasicAuth = null;
if (basicAuthUser && basicAuthPw && isBasicAuthEnabled) {
httpBasicAuth = {
basicAuth: {
users: [basicAuthUser + ':' + await hashPassword(basicAuthPw, 1)]
}
};
}
if (buildPack === 'compose') {
const services = Object.entries(JSON.parse(dockerComposeConfiguration));
if (services.length > 0) {
@@ -404,27 +438,33 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
traefik.http.routers = {
...traefik.http.routers,
...generateRouters(
...await generateRouters({
serviceId,
domain,
nakedDomain,
pathPrefix,
isHttps,
isWWW,
dualCerts,
isCustomSSL
)
isDualCerts: dualCerts,
isCustomSSL,
httpBasicAuth
})
};
traefik.http.services = {
...traefik.http.services,
...generateServices(serviceId, containerId, port)
};
if (httpBasicAuth) {
traefik.http.middlewares[`${serviceId}-${pathPrefix}-basic-auth`] = {
...httpBasicAuth
};
}
}
}
}
continue;
}
const { previews, dualCerts, isCustomSSL, isHttp2 } = settings;
const { previews, dualCerts, isCustomSSL, isHttp2, basicAuth } = settings;
const { network, id: dockerId } = destinationDocker;
if (!fqdn) {
continue;
@@ -437,22 +477,28 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
const serviceId = `${id}-${port || 'default'}`;
traefik.http.routers = {
...traefik.http.routers,
...generateRouters(
...await generateRouters({
serviceId,
domain,
nakedDomain,
pathPrefix,
isHttps,
isWWW,
dualCerts,
isDualCerts: dualCerts,
isCustomSSL,
isHttp2
)
isHttp2,
httpBasicAuth
})
};
traefik.http.services = {
...traefik.http.services,
...generateServices(serviceId, id, port, isHttp2, isHttps)
};
if (httpBasicAuth) {
traefik.http.middlewares[`${serviceId}-${pathPrefix}-basic-auth`] = {
...httpBasicAuth
};
}
if (previews) {
const { stdout } = await executeCommand({
dockerId,
@@ -466,29 +512,35 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
.map((c) => c.replace(/"/g, ''));
if (containers.length > 0) {
for (const container of containers) {
const previewDomain = `${container.split('-')[1]}${
coolifySettings.previewSeparator
}${domain}`;
const previewDomain = `${container.split('-')[1]}${coolifySettings.previewSeparator
}${domain}`;
const nakedDomain = previewDomain.replace(/^www\./, '');
const pathPrefix = '/';
const serviceId = `${container}-${port || 'default'}`;
traefik.http.routers = {
...traefik.http.routers,
...generateRouters(
...await generateRouters({
serviceId,
previewDomain,
domain: previewDomain,
nakedDomain,
pathPrefix,
isHttps,
isWWW,
dualCerts,
isCustomSSL
)
isDualCerts: dualCerts,
isCustomSSL,
isHttp2: false,
httpBasicAuth
})
};
traefik.http.services = {
...traefik.http.services,
...generateServices(serviceId, container, port, isHttp2)
};
if (httpBasicAuth) {
traefik.http.middlewares[`${serviceId}-${pathPrefix}-basic-auth`] = {
...httpBasicAuth
};
}
}
}
}
@@ -542,7 +594,7 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
if (isDomainAndProxyConfiguration.length > 0) {
const template: any = await parseAndFindServiceTemplates(service, null, true);
const { proxy } = template.services[oneService] || found.services[oneService];
for (let configuration of proxy) {
for (const configuration of proxy) {
if (configuration.hostPort) {
continue;
}
@@ -582,16 +634,16 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
const serviceId = `${oneService}-${port || 'default'}`;
traefik.http.routers = {
...traefik.http.routers,
...generateRouters(
...await generateRouters({
serviceId,
domain,
nakedDomain,
pathPrefix,
isHttps,
isWWW,
dualCerts,
isCustomSSL
)
isDualCerts: dualCerts,
isCustomSSL,
})
};
traefik.http.services = {
...traefik.http.services,
@@ -619,16 +671,16 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
const serviceId = `${oneService}-${port || 'default'}`;
traefik.http.routers = {
...traefik.http.routers,
...generateRouters(
...await generateRouters({
serviceId,
domain,
nakedDomain,
pathPrefix,
isHttps,
isWWW,
dualCerts,
isDualCerts: dualCerts,
isCustomSSL
)
})
};
traefik.http.services = {
...traefik.http.services,
@@ -660,16 +712,16 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
const serviceId = `${id}-${port || 'default'}`;
traefik.http.routers = {
...traefik.http.routers,
...generateRouters(
...await generateRouters({
serviceId,
domain,
nakedDomain,
pathPrefix,
isHttps,
isWWW,
dualCerts,
isDualCerts: dualCerts,
isCustomSSL
)
})
};
traefik.http.services = {
...traefik.http.services,

View File

@@ -4,9 +4,9 @@ import { proxyConfiguration, otherProxyConfiguration } from './handlers';
import { OtherProxyConfiguration } from './types';
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get<OnlyId>('/main.json', async (request, reply) => proxyConfiguration(request, false));
fastify.get<OnlyId>('/main.json', async (request) => proxyConfiguration(request, false));
fastify.get<OnlyId>('/remote/:id', async (request) => proxyConfiguration(request, true));
fastify.get<OtherProxyConfiguration>('/other.json', async (request, reply) =>
fastify.get<OtherProxyConfiguration>('/other.json', async (request) =>
otherProxyConfiguration(request)
);
};

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,6 @@
import { dev } from '$app/env';
import Cookies from 'js-cookie';
import { dashify } from './common';
export function getAPIUrl() {
if (GITPOD_WORKSPACE_URL) {
@@ -72,17 +73,19 @@ async function send({
...headers
};
}
if (token && !path.startsWith('https://')) {
if (token && !path.startsWith('https://') && !path.startsWith('http://')) {
opts.headers = {
...opts.headers,
Authorization: `Bearer ${token}`
};
}
if (!path.startsWith('https://')) {
if (!path.startsWith('https://') && !path.startsWith('http://')) {
path = `/api/v1${path}`;
}
if (dev && !path.startsWith('https://')) {
if (dev && !path.startsWith('https://') && !path.startsWith('http://')) {
path = `${getAPIUrl()}${path}`;
}
if (method === 'POST' && data && !opts.body) {
@@ -100,6 +103,14 @@ async function send({
responseData = await response.json();
} else if (contentType?.indexOf('text/plain') !== -1) {
responseData = await response.text();
} else if (contentType?.indexOf('application/octet-stream') !== -1) {
responseData = await response.blob();
const fileName = dashify(data.id + '-' + data.name)
const fileLink = document.createElement('a');
fileLink.href = URL.createObjectURL(new Blob([responseData]))
fileLink.download = fileName + '.gz';
fileLink.click();
fileLink.remove();
} else {
return {};
}

View File

@@ -196,6 +196,9 @@
"domain_fqdn": "Domain (FQDN)",
"https_explainer": "If you specify <span class='text-settings '>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-settings '>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 '>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?",
"basic_auth": "Basic Auth",
"basic_auth_user": "User",
"basic_auth_pw": "Password",
"ssl_explainer": "It will generate certificates for both www and non-www. <br>You need to have <span class=' text-settings'>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",

View File

@@ -260,7 +260,7 @@
<PublicRepository />
<div class="flex flex-row items-center pt-10">
<div class="title py-4 pr-4">Simple Dockerfile <Beta /></div>
<DocLink url="https://docs.coollabs.io/coolify-v3/applications/#dockerfile" />
<DocLink url="https://docs.coollabs.io/coolify-v3/applications/#simple-dockerfile" />
</div>
<div class="mx-auto max-w-screen-2xl">
<form class="flex flex-col" on:submit|preventDefault={handleDockerImage}>

View File

@@ -29,27 +29,28 @@
export let application: any;
export let settings: any;
import yaml from 'js-yaml';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { onMount } from 'svelte';
import Select from 'svelte-select';
import { get, getAPIUrl, post } from '$lib/api';
import cuid from 'cuid';
import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common';
import Beta from '$lib/components/Beta.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import Setting from '$lib/components/Setting.svelte';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import {
addToast,
appSession,
checkIfDeploymentEnabledApplications,
setLocation,
status,
features,
isDeploymentEnabled,
features
setLocation,
status
} from '$lib/store';
import { t } from '$lib/translations';
import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common';
import Setting from '$lib/components/Setting.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { goto } from '$app/navigation';
import Beta from '$lib/components/Beta.svelte';
import cuid from 'cuid';
import yaml from 'js-yaml';
import { onMount } from 'svelte';
import Select from 'svelte-select';
import { saveForm } from './utils';
const { id } = $page.params;
@@ -77,6 +78,7 @@
let isCustomSSL = application.settings?.isCustomSSL;
let autodeploy = application.settings?.autodeploy;
let isBot = application.settings?.isBot;
let basicAuth = application.settings?.basicAuth;
let isDBBranching = application.settings?.isDBBranching;
let htmlUrl = application.gitSource?.htmlUrl;
let isHttp2 = application.settings.isHttp2;
@@ -186,6 +188,9 @@
if (name === 'isCustomSSL') {
isCustomSSL = !isCustomSSL;
}
if (name === 'basicAuth') {
basicAuth = !basicAuth;
}
if (name === 'isBot') {
if ($status.application.overallStatus !== 'stopped') return;
isBot = !isBot;
@@ -210,7 +215,8 @@
isCustomSSL,
isHttp2,
branch: application.branch,
projectId: application.projectId
projectId: application.projectId,
basicAuth
});
return addToast({
message: $t('application.settings_saved'),
@@ -232,6 +238,9 @@
if (name === 'isBot') {
isBot = !isBot;
}
if (name === 'basicAuth') {
basicAuth = !basicAuth;
}
if (name === 'isDBBranching') {
isDBBranching = !isDBBranching;
}
@@ -272,6 +281,7 @@
}
}
}
console.log(application);
await saveForm(id, application, baseDatabaseBranch, dockerComposeConfiguration);
setLocation(application, settings);
$isDeploymentEnabled = checkIfDeploymentEnabledApplications(application);
@@ -498,7 +508,7 @@
<div class="title font-bold pb-3">General</div>
{#if $appSession.isAdmin}
<button
class="btn btn-sm btn-primary"
class="btn btn-sm btn-primary"
type="submit"
class:loading={loading.save}
class:bg-orange-600={forceSave}
@@ -751,7 +761,7 @@
on:click={() => !isDisabled && changeSettings('dualCerts')}
/>
</div>
{#if isHttps && application.buildPack !== 'compose'}
<div class="grid grid-cols-2 items-center pb-4">
<Setting
@@ -774,6 +784,46 @@
on:click={() => changeSettings('isHttp2')}
/>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
id="basicAuth"
isCenter={false}
bind:setting={basicAuth}
title={$t('application.basic_auth')}
description="Activate basic authentication for your application. <br>Useful if you want to protect your application with a password. <br><br>Use the <span class='font-bold text-settings'>username</span> and <span class='font-bold text-settings'>password</span> fields to set the credentials."
on:click={() => changeSettings('basicAuth')}
/>
</div>
{#if basicAuth}
<div class="grid grid-cols-2 items-center">
<label for="basicAuthUser">{$t('application.basic_auth_user')}</label>
<input
bind:this={fqdnEl}
class="w-full"
required={!application.settings?.basicAuth}
name="basicAuthUser"
id="basicAuthUser"
class:border={!application.settings?.basicAuth && !application.basicAuthUser}
class:border-red-500={!application.settings?.basicAuth &&
!application.basicAuthUser}
bind:value={application.basicAuthUser}
placeholder="eg: admin"
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="basicAuthPw">{$t('application.basic_auth_pw')}</label>
<CopyPasswordField
bind:this={fqdnEl}
isPasswordField={true}
required={!application.settings?.basicAuth}
name="basicAuthPw"
id="basicAuthPw"
bind:value={application.basicAuthPw}
placeholder="**********"
/>
</div>
{/if}
{/if}
</div>
{#if isSimpleDockerfile}
@@ -782,7 +832,7 @@
</div>
<div class="grid grid-flow-row gap-2 px-4 pr-5">
<div class="grid grid-cols-2 items-center pt-4">
<div class="grid grid-cols-2 items-center pt-4">
<label for="simpleDockerfile">Dockerfile</label>
<div class="flex gap-2">
<textarea

View File

@@ -13,17 +13,19 @@
import Redis from './_Redis.svelte';
import CouchDb from './_CouchDb.svelte';
import EdgeDB from './_EdgeDB.svelte';
import { post } from '$lib/api';
import { get, post } from '$lib/api';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common';
import { addToast, appSession, status } from '$lib/store';
import Explainer from '$lib/components/Explainer.svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
const { id } = $page.params;
let loading = {
main: false,
public: false
public: false,
backup: false
};
let publicUrl = '';
let appendOnly = database.settings.appendOnly;
@@ -109,6 +111,7 @@
if ($status.database.isPublic) {
database.publicPort = publicPort;
}
generateUrl();
} catch (error) {
return errorNotification(error);
} finally {
@@ -130,6 +133,22 @@
loading.main = false;
}
}
async function backupDatabase() {
try {
loading.backup = true;
addToast({
message:
'Backup will be downloaded soon and saved to /var/lib/docker/volumes/coolify-local-backup/_data/ on the host system.',
type: 'success',
timeout: 15000
});
return await post(`/databases/${id}/backup`, { id, name: database.name });
} catch (error) {
return errorNotification(error);
} finally {
loading.backup = false;
}
}
</script>
<div class="mx-auto max-w-6xl p-4">
@@ -144,6 +163,19 @@
class:bg-databases={!loading.main}
disabled={loading.main}>{$t('forms.save')}</button
>
{#if database.type !== 'redis' && database.type !== 'edgedb'}
{#if $status.database.isRunning}
<button
class="btn btn-sm"
on:click={backupDatabase}
class:loading={loading.backup}
class:bg-databases={!loading.backup}
disabled={loading.backup}>Backup Database</button
>
{:else}
<button disabled class="btn btn-sm">Backup Database (start the database)</button>
{/if}
{/if}
{/if}
</div>
<div class="grid gap-2 grid-cols-2 auto-rows-max lg:px-10 px-2">

View File

@@ -56,23 +56,23 @@
async function rollback() {
if (rollbackVersion) {
const sure = confirm(`Are you sure you want rollback Coolify to ${rollbackVersion}?`);
const sure = confirm(`Are you sure you want upgrade Coolify to ${rollbackVersion}?`);
if (sure) {
try {
loading.rollback = true;
console.log('loading.rollback', loading.rollback);
if (dev) {
console.log('rolling back to', rollbackVersion);
console.log('Upgrading to ', rollbackVersion);
await asyncSleep(4000);
return window.location.reload();
} else {
addToast({
message: 'Rollback started...',
message: 'Upgrade started...',
type: 'success'
});
await post(`/update`, { type: 'update', latestVersion: rollbackVersion });
addToast({
message: 'Rollback completed.<br><br>Waiting for the new version to start...',
message: 'Upgrade completed.<br><br>Waiting for the new version to start...',
type: 'success'
});
@@ -381,12 +381,12 @@
/>
</div>
<div class="grid grid-cols-4 items-center">
<div class="grid grid-cols-4 items-center pb-12">
<div class="col-span-2">
Rollback Coolify to a specific version
Upgrade Coolify to a specific version
<Explainer
position="dropdown-bottom"
explanation="You can rollback to a specific version of Coolify. This will not affect your current running resources.<br><br><a href='https://github.com/coollabsio/coolify/releases' target='_blank'>See available versions</a>"
explanation="You can upgrade to a specific version of Coolify. This will not affect your current running resources, but could cause issues if you downgrade to an older version where the database layout was different..<br><br><a href='https://github.com/coollabsio/coolify/releases' target='_blank'>See available versions</a>"
/>
</div>
<input
@@ -401,7 +401,7 @@
class:loading={loading.rollback}
class="btn btn-primary ml-2"
disabled={!rollbackVersion || loading.rollback}
on:click|preventDefault|stopPropagation={rollback}>Rollback</button
on:click|preventDefault|stopPropagation={rollback}>Upgrade</button
>
</div>
<div class="grid grid-cols-2 items-center">

View File

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

1279
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff