Merge pull request #380 from coollabsio/next

v2.6.0
This commit is contained in:
Andras Bacsai 2022-05-02 14:59:40 +02:00 committed by GitHub
commit 2315192f4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
83 changed files with 2832 additions and 393 deletions

View File

@ -13,10 +13,16 @@ ## How to install
Installation is automated with the following command:
```bash
/bin/bash -c "$(curl -fsSL https://get.coollabs.io/coolify/install.sh)"
wget -q https://get.coollabs.io/coolify/install.sh -O install.sh; sudo bash ./install.sh
```
If you would like no questions during installation
If you would like no questions during installation:
```bash
wget -q https://get.coollabs.io/coolify/install.sh -O install.sh; sudo bash ./install.sh -f
```
For more details goto the [docs](https://docs.coollabs.io/coolify/installation).
## Features
@ -77,6 +83,8 @@ ### One-click services
- [LanguageTool](https://languagetool.org)
- [n8n](https://n8n.io)
- [Uptime Kuma](https://github.com/louislam/uptime-kuma)
- [MeiliSearch](https://github.com/meilisearch/meilisearch)
- [Umami](https://github.com/mikecao/umami)
## Migration from v1

View File

@ -1,7 +1,7 @@
{
"name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "2.5.2",
"version": "2.6.0",
"license": "AGPL-3.0",
"scripts": {
"dev": "docker-compose -f docker-compose-dev.yaml up -d && cross-env NODE_ENV=development & svelte-kit dev --host 0.0.0.0",
@ -55,8 +55,8 @@
"svelte-check": "2.7.0",
"svelte-preprocess": "4.10.6",
"svelte-select": "4.4.7",
"tailwindcss": "3.0.24",
"sveltekit-i18n": "2.1.2",
"tailwindcss": "3.0.24",
"ts-node": "10.7.0",
"tslib": "2.3.1",
"typescript": "4.6.3"

View File

@ -48,8 +48,8 @@ specifiers:
svelte-kit-cookie-session: 2.1.3
svelte-preprocess: 4.10.6
svelte-select: 4.4.7
tailwindcss: 3.0.24
sveltekit-i18n: 2.1.2
tailwindcss: 3.0.24
tailwindcss-scrollbar: 0.1.0
ts-node: 10.7.0
tslib: 2.3.1
@ -108,9 +108,9 @@ devDependencies:
svelte-check: 2.7.0_postcss@8.4.12+svelte@3.47.0
svelte-preprocess: 4.10.6_41810887ae6c6d59323116f47e33fa38
svelte-select: 4.4.7
sveltekit-i18n: 2.1.2_svelte@3.47.0
tailwindcss: 3.0.24_ts-node@10.7.0
ts-node: 10.7.0_de7c86b0cde507c63a0402da5b982bd3
sveltekit-i18n: 2.1.2_svelte@3.46.4
tslib: 2.3.1
typescript: 4.6.3
@ -424,7 +424,7 @@ packages:
- supports-color
dev: true
/@sveltekit-i18n/base/1.1.1_svelte@3.46.4:
/@sveltekit-i18n/base/1.1.1_svelte@3.47.0:
resolution:
{
integrity: sha512-J/sMU0OwS3dCLOuilHMBqu8vZHuuXiNV9vFJx8Nb4/b5BlR/KCZ4bCXI8wZR02GHeCOYKZxWus07CM1scxa/jw==
@ -432,7 +432,7 @@ packages:
peerDependencies:
svelte: ^3.x
dependencies:
svelte: 3.46.4
svelte: 3.47.0
optionalDependencies:
'@sveltekit-i18n/parser-default': 1.0.3
dev: true
@ -4977,7 +4977,7 @@ packages:
engines: { node: '>= 8' }
dev: true
/sveltekit-i18n/2.1.2_svelte@3.46.4:
/sveltekit-i18n/2.1.2_svelte@3.47.0:
resolution:
{
integrity: sha512-s5YxcbNd2EWNZaZR1A4Drt8s53E4fpUkN4XIWd3VRpw1pihZVWssqmBW1qkjQ6AB0kiu1Qwule+vt1HkbQOjrg==
@ -4985,9 +4985,9 @@ packages:
peerDependencies:
svelte: ^3.x
dependencies:
'@sveltekit-i18n/base': 1.1.1_svelte@3.46.4
'@sveltekit-i18n/base': 1.1.1_svelte@3.47.0
'@sveltekit-i18n/parser-default': 1.0.3
svelte: 3.46.4
svelte: 3.47.0
dev: true
/table/6.7.2:

View File

@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "Application" ADD COLUMN "baseBuildImage" TEXT;
ALTER TABLE "Application" ADD COLUMN "baseImage" TEXT;

View File

@ -0,0 +1,16 @@
-- CreateTable
CREATE TABLE "Hasura" (
"id" TEXT NOT NULL PRIMARY KEY,
"serviceId" TEXT NOT NULL,
"postgresqlUser" TEXT NOT NULL,
"postgresqlPassword" TEXT NOT NULL,
"postgresqlDatabase" TEXT NOT NULL,
"postgresqlPublicPort" INTEGER,
"graphQLAdminPassword" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Hasura_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "Hasura_serviceId_key" ON "Hasura"("serviceId");

View File

@ -0,0 +1,25 @@
-- CreateTable
CREATE TABLE "Fider" (
"id" TEXT NOT NULL PRIMARY KEY,
"serviceId" TEXT NOT NULL,
"postgresqlUser" TEXT NOT NULL,
"postgresqlPassword" TEXT NOT NULL,
"postgresqlDatabase" TEXT NOT NULL,
"postgresqlPublicPort" INTEGER,
"jwtSecret" TEXT NOT NULL,
"emailNoreply" TEXT,
"emailMailgunApiKey" TEXT,
"emailMailgunDomain" TEXT,
"emailMailgunRegion" TEXT,
"emailSmtpHost" TEXT,
"emailSmtpPort" INTEGER,
"emailSmtpUser" TEXT,
"emailSmtpPassword" TEXT,
"emailSmtpEnableStartTls" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Fider_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "Fider_serviceId_key" ON "Fider"("serviceId");

View File

@ -0,0 +1,29 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Fider" (
"id" TEXT NOT NULL PRIMARY KEY,
"serviceId" TEXT NOT NULL,
"postgresqlUser" TEXT NOT NULL,
"postgresqlPassword" TEXT NOT NULL,
"postgresqlDatabase" TEXT NOT NULL,
"postgresqlPublicPort" INTEGER,
"jwtSecret" TEXT NOT NULL,
"emailNoreply" TEXT,
"emailMailgunApiKey" TEXT,
"emailMailgunDomain" TEXT,
"emailMailgunRegion" TEXT NOT NULL DEFAULT 'EU',
"emailSmtpHost" TEXT,
"emailSmtpPort" INTEGER,
"emailSmtpUser" TEXT,
"emailSmtpPassword" TEXT,
"emailSmtpEnableStartTls" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Fider_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_Fider" ("createdAt", "emailMailgunApiKey", "emailMailgunDomain", "emailMailgunRegion", "emailNoreply", "emailSmtpEnableStartTls", "emailSmtpHost", "emailSmtpPassword", "emailSmtpPort", "emailSmtpUser", "id", "jwtSecret", "postgresqlDatabase", "postgresqlPassword", "postgresqlPublicPort", "postgresqlUser", "serviceId", "updatedAt") SELECT "createdAt", "emailMailgunApiKey", "emailMailgunDomain", coalesce("emailMailgunRegion", 'EU') AS "emailMailgunRegion", "emailNoreply", "emailSmtpEnableStartTls", "emailSmtpHost", "emailSmtpPassword", "emailSmtpPort", "emailSmtpUser", "id", "jwtSecret", "postgresqlDatabase", "postgresqlPassword", "postgresqlPublicPort", "postgresqlUser", "serviceId", "updatedAt" FROM "Fider";
DROP TABLE "Fider";
ALTER TABLE "new_Fider" RENAME TO "Fider";
CREATE UNIQUE INDEX "Fider_serviceId_key" ON "Fider"("serviceId");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@ -0,0 +1,23 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Setting" (
"id" TEXT NOT NULL PRIMARY KEY,
"fqdn" TEXT,
"isRegistrationEnabled" BOOLEAN NOT NULL DEFAULT false,
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
"minPort" INTEGER NOT NULL DEFAULT 9000,
"maxPort" INTEGER NOT NULL DEFAULT 9100,
"proxyPassword" TEXT NOT NULL,
"proxyUser" TEXT NOT NULL,
"proxyHash" TEXT,
"isAutoUpdateEnabled" BOOLEAN NOT NULL DEFAULT false,
"isDNSCheckEnabled" BOOLEAN NOT NULL DEFAULT true,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_Setting" ("createdAt", "dualCerts", "fqdn", "id", "isAutoUpdateEnabled", "isRegistrationEnabled", "maxPort", "minPort", "proxyHash", "proxyPassword", "proxyUser", "updatedAt") SELECT "createdAt", "dualCerts", "fqdn", "id", "isAutoUpdateEnabled", "isRegistrationEnabled", "maxPort", "minPort", "proxyHash", "proxyPassword", "proxyUser", "updatedAt" FROM "Setting";
DROP TABLE "Setting";
ALTER TABLE "new_Setting" RENAME TO "Setting";
CREATE UNIQUE INDEX "Setting_fqdn_key" ON "Setting"("fqdn");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@ -19,6 +19,7 @@ model Setting {
proxyUser String
proxyHash String?
isAutoUpdateEnabled Boolean @default(false)
isDNSCheckEnabled Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
@ -105,6 +106,8 @@ model Application {
gitSource GitSource? @relation(fields: [gitSourceId], references: [id])
secrets Secret[]
persistentStorage ApplicationPersistentStorage[]
baseImage String?
baseBuildImage String?
}
model ApplicationSettings {
@ -303,6 +306,8 @@ model Service {
meiliSearch MeiliSearch?
persistentStorage ServicePersistentStorage[]
umami Umami?
hasura Hasura?
fider Fider?
}
model PlausibleAnalytics {
@ -401,3 +406,38 @@ model Umami {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Hasura {
id String @id @default(cuid())
serviceId String @unique
postgresqlUser String
postgresqlPassword String
postgresqlDatabase String
postgresqlPublicPort Int?
graphQLAdminPassword String
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Fider {
id String @id @default(cuid())
serviceId String @unique
postgresqlUser String
postgresqlPassword String
postgresqlDatabase String
postgresqlPublicPort Int?
jwtSecret String
emailNoreply String?
emailMailgunApiKey String?
emailMailgunDomain String?
emailMailgunRegion String @default("EU")
emailSmtpHost String?
emailSmtpPort Int?
emailSmtpUser String?
emailSmtpPassword String?
emailSmtpEnableStartTls Boolean @default(false)
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

View File

@ -5,6 +5,19 @@ import { scanningTemplates } from '$lib/components/templates';
import { promises as fs } from 'fs';
import { staticDeployments } from '$lib/components/common';
const staticApps = ['static', 'react', 'vuejs', 'svelte', 'gatsby', 'astro', 'eleventy'];
const nodeBased = [
'react',
'vuejs',
'svelte',
'gatsby',
'php',
'astro',
'eleventy',
'node',
'nestjs'
];
export function makeLabelForStandaloneApplication({
applicationId,
fqdn,
@ -104,11 +117,12 @@ export const setDefaultConfiguration = async (data) => {
else if (buildPack === 'php') port = 80;
else if (buildPack === 'python') port = 8000;
}
if (!installCommand && buildPack !== 'static')
if (!installCommand && buildPack !== 'static' && buildPack !== 'laravel')
installCommand = template?.installCommand || 'yarn install';
if (!startCommand && buildPack !== 'static')
if (!startCommand && buildPack !== 'static' && buildPack !== 'laravel')
startCommand = template?.startCommand || 'yarn start';
if (!buildCommand && buildPack !== 'static') buildCommand = template?.buildCommand || null;
if (!buildCommand && buildPack !== 'static' && buildPack !== 'laravel')
buildCommand = template?.buildCommand || null;
if (!publishDirectory) publishDirectory = template?.publishDirectory || null;
if (baseDirectory) {
if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`;
@ -137,7 +151,13 @@ export const setDefaultConfiguration = async (data) => {
};
};
export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId) {
export async function copyBaseConfigurationFiles(
buildPack,
workdir,
buildId,
applicationId,
baseImage
) {
try {
if (buildPack === 'php') {
await fs.writeFile(`${workdir}/entrypoint.sh`, `chown -R 1000 /app`);
@ -146,7 +166,7 @@ export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, ap
buildId,
applicationId
});
} else if (staticDeployments.includes(buildPack)) {
} else if (staticDeployments.includes(buildPack) && baseImage.includes('nginx')) {
await fs.writeFile(
`${workdir}/nginx.conf`,
`user nginx;
@ -174,7 +194,7 @@ export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, ap
include /etc/nginx/mime.types;
default_type application/octet-stream;
server {
listen 80;
server_name localhost;
@ -199,11 +219,6 @@ export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, ap
}
`
);
await saveBuildLog({
line: 'Copied default configuration file for Nginx.',
buildId,
applicationId
});
}
} catch (error) {
console.log(error);
@ -218,3 +233,215 @@ export function checkPnpm(installCommand = null, buildCommand = null, startComma
startCommand?.includes('pnpm')
);
}
export function setDefaultBaseImage(buildPack) {
const nodeVersions = [
{
value: 'node:lts',
label: 'node:lts'
},
{
value: 'node:18',
label: 'node:18'
},
{
value: 'node:17',
label: 'node:17'
},
{
value: 'node:16',
label: 'node:16'
},
{
value: 'node:14',
label: 'node:14'
},
{
value: 'node:12',
label: 'node:12'
}
];
const staticVersions = [
{
value: 'webdevops/nginx:alpine',
label: 'webdevops/nginx:alpine'
},
{
value: 'webdevops/apache:alpine',
label: 'webdevops/apache:alpine'
}
];
const rustVersions = [
{
value: 'rust:latest',
label: 'rust:latest'
},
{
value: 'rust:1.60',
label: 'rust:1.60'
},
{
value: 'rust:1.60-buster',
label: 'rust:1.60-buster'
},
{
value: 'rust:1.60-bullseye',
label: 'rust:1.60-bullseye'
},
{
value: 'rust:1.60-slim-buster',
label: 'rust:1.60-slim-buster'
},
{
value: 'rust:1.60-slim-bullseye',
label: 'rust:1.60-slim-bullseye'
},
{
value: 'rust:1.60-alpine3.14',
label: 'rust:1.60-alpine3.14'
},
{
value: 'rust:1.60-alpine3.15',
label: 'rust:1.60-alpine3.15'
}
];
const phpVersions = [
{
value: 'webdevops/php-apache:8.0',
label: 'webdevops/php-apache:8.0'
},
{
value: 'webdevops/php-nginx:8.0',
label: 'webdevops/php-nginx:8.0'
},
{
value: 'webdevops/php-apache:7.4',
label: 'webdevops/php-apache:7.4'
},
{
value: 'webdevops/php-nginx:7.4',
label: 'webdevops/php-nginx:7.4'
},
{
value: 'webdevops/php-apache:7.3',
label: 'webdevops/php-apache:7.3'
},
{
value: 'webdevops/php-nginx:7.3',
label: 'webdevops/php-nginx:7.3'
},
{
value: 'webdevops/php-apache:7.2',
label: 'webdevops/php-apache:7.2'
},
{
value: 'webdevops/php-nginx:7.2',
label: 'webdevops/php-nginx:7.2'
},
{
value: 'webdevops/php-apache:7.1',
label: 'webdevops/php-apache:7.1'
},
{
value: 'webdevops/php-nginx:7.1',
label: 'webdevops/php-nginx:7.1'
},
{
value: 'webdevops/php-apache:7.0',
label: 'webdevops/php-apache:7.0'
},
{
value: 'webdevops/php-nginx:7.0',
label: 'webdevops/php-nginx:7.0'
},
{
value: 'webdevops/php-apache:5.6',
label: 'webdevops/php-apache:5.6'
},
{
value: 'webdevops/php-nginx:5.6',
label: 'webdevops/php-nginx:5.6'
},
{
value: 'webdevops/php-apache:8.0-alpine',
label: 'webdevops/php-apache:8.0-alpine'
},
{
value: 'webdevops/php-nginx:8.0-alpine',
label: 'webdevops/php-nginx:8.0-alpine'
},
{
value: 'webdevops/php-apache:7.4-alpine',
label: 'webdevops/php-apache:7.4-alpine'
},
{
value: 'webdevops/php-nginx:7.4-alpine',
label: 'webdevops/php-nginx:7.4-alpine'
},
{
value: 'webdevops/php-apache:7.3-alpine',
label: 'webdevops/php-apache:7.3-alpine'
},
{
value: 'webdevops/php-nginx:7.3-alpine',
label: 'webdevops/php-nginx:7.3-alpine'
},
{
value: 'webdevops/php-apache:7.2-alpine',
label: 'webdevops/php-apache:7.2-alpine'
},
{
value: 'webdevops/php-nginx:7.2-alpine',
label: 'webdevops/php-nginx:7.2-alpine'
},
{
value: 'webdevops/php-apache:7.1-alpine',
label: 'webdevops/php-apache:7.1-alpine'
},
{
value: 'webdevops/php-nginx:7.1-alpine',
label: 'webdevops/php-nginx:7.1-alpine'
}
];
let payload = {
baseImage: null,
baseBuildImage: null,
baseImages: [],
baseBuildImages: []
};
if (nodeBased.includes(buildPack)) {
payload.baseImage = 'node:lts';
payload.baseImages = nodeVersions;
payload.baseBuildImage = 'node:lts';
payload.baseBuildImages = nodeVersions;
}
if (staticApps.includes(buildPack)) {
payload.baseImage = 'webdevops/nginx:alpine';
payload.baseImages = staticVersions;
payload.baseBuildImage = 'node:lts';
payload.baseBuildImages = nodeVersions;
}
if (buildPack === 'python') {
payload.baseImage = 'python:3-alpine';
}
if (buildPack === 'rust') {
payload.baseImage = 'rust:latest';
payload.baseBuildImage = 'rust:latest';
payload.baseImages = rustVersions;
payload.baseBuildImages = rustVersions;
}
if (buildPack === 'deno') {
payload.baseImage = 'denoland/deno:latest';
}
if (buildPack === 'php') {
payload.baseImage = 'webdevops/php-apache:8.0-alpine';
payload.baseImages = phpVersions;
}
if (buildPack === 'laravel') {
payload.baseImage = 'webdevops/php-apache:8.0-alpine';
payload.baseBuildImage = 'node:18';
payload.baseBuildImages = nodeVersions;
}
return payload;
}

View File

@ -2,8 +2,16 @@ 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 {
workdir,
port,
baseDirectory,
secrets,
pullmergeRequestId,
denoMainFile,
denoOptions,
buildId
} = data;
const Dockerfile: Array<string> = [];
let depsFound = false;
@ -14,7 +22,7 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
@ -45,8 +53,8 @@ const createDockerfile = async (data, image): Promise<void> => {
export default async function (data) {
try {
const image = 'denoland/deno:latest';
await createDockerfile(data, image);
const { baseImage, baseBuildImage } = data;
await createDockerfile(data, baseImage);
await buildImage(data);
} catch (error) {
throw error;

View File

@ -24,6 +24,7 @@ export default async function ({
.toString()
.trim()
.split('\n');
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
@ -42,6 +43,7 @@ export default async function ({
}
});
}
await fs.writeFile(`${dockerFileOut}${dockerFileLocation}`, Dockerfile.join('\n'));
await buildImage({ applicationId, tag, workdir, docker, buildId, debug, dockerFileLocation });
} catch (error) {

View File

@ -2,25 +2,25 @@ import { buildCacheImageWithNode, buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
const createDockerfile = async (data, imageforBuild): Promise<void> => {
const { applicationId, tag, workdir, publishDirectory } = data;
const { applicationId, tag, workdir, publishDirectory, baseImage, buildId } = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageforBuild}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
if (baseImage.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
}
Dockerfile.push(`EXPOSE 80`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
};
export default async function (data) {
try {
const image = 'webdevops/nginx:alpine';
const imageForBuild = 'node:lts';
await buildCacheImageWithNode(data, imageForBuild);
await createDockerfile(data, image);
const { baseImage, baseBuildImage } = data;
await buildCacheImageWithNode(data, baseImage);
await createDockerfile(data, baseBuildImage);
await buildImage(data);
} catch (error) {
throw error;

View File

@ -14,6 +14,7 @@ import astro from './static';
import eleventy from './static';
import python from './python';
import deno from './deno';
import laravel from './laravel';
export {
node,
@ -31,5 +32,6 @@ export {
astro,
eleventy,
python,
deno
deno,
laravel
};

View File

@ -0,0 +1,40 @@
import { buildCacheImageForLaravel, buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => {
const { workdir, applicationId, tag, buildId } = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`ENV WEB_DOCUMENT_ROOT /app/public`);
Dockerfile.push(`COPY --chown=application:application composer.* ./`);
Dockerfile.push(`COPY --chown=application:application database/ database/`);
Dockerfile.push(
`RUN composer install --ignore-platform-reqs --no-interaction --no-plugins --no-scripts --prefer-dist`
);
Dockerfile.push(
`COPY --chown=application:application --from=${applicationId}:${tag}-cache /app/public/js/ /app/public/js/`
);
Dockerfile.push(
`COPY --chown=application:application --from=${applicationId}:${tag}-cache /app/public/css/ /app/public/css/`
);
Dockerfile.push(
`COPY --chown=application:application --from=${applicationId}:${tag}-cache /app/mix-manifest.json /app/public/mix-manifest.json`
);
Dockerfile.push(`COPY --chown=application:application . ./`);
Dockerfile.push(`EXPOSE 80`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
};
export default async function (data) {
const { baseImage, baseBuildImage } = data;
try {
await buildCacheImageForLaravel(data, baseBuildImage);
await createDockerfile(data, baseImage);
await buildImage(data);
} catch (error) {
throw error;
}
}

View File

@ -2,13 +2,13 @@ import { buildCacheImageWithNode, buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => {
const { applicationId, tag, port, startCommand, workdir, baseDirectory } = data;
const { buildId, applicationId, tag, port, startCommand, workdir, baseDirectory } = data;
const Dockerfile: Array<string> = [];
const isPnpm = startCommand.includes('pnpm');
Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
if (isPnpm) {
Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm');
Dockerfile.push('RUN pnpm add -g pnpm');
@ -22,11 +22,9 @@ const createDockerfile = async (data, image): Promise<void> => {
export default async function (data) {
try {
const image = 'node:lts';
const imageForBuild = 'node:lts';
await buildCacheImageWithNode(data, imageForBuild);
await createDockerfile(data, image);
const { baseImage, baseBuildImage } = data;
await buildCacheImageWithNode(data, baseBuildImage);
await createDockerfile(data, baseImage);
await buildImage(data);
} catch (error) {
throw error;

View File

@ -4,6 +4,7 @@ import { checkPnpm } from './common';
const createDockerfile = async (data, image): Promise<void> => {
const {
buildId,
workdir,
port,
installCommand,
@ -17,7 +18,7 @@ const createDockerfile = async (data, image): Promise<void> => {
const isPnpm = checkPnpm(installCommand, buildCommand, startCommand);
Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
@ -50,8 +51,8 @@ const createDockerfile = async (data, image): Promise<void> => {
export default async function (data) {
try {
const image = 'node:lts';
await createDockerfile(data, image);
const { baseImage, baseBuildImage } = data;
await createDockerfile(data, baseImage);
await buildImage(data);
} catch (error) {
throw error;

View File

@ -11,14 +11,15 @@ const createDockerfile = async (data, image): Promise<void> => {
startCommand,
baseDirectory,
secrets,
pullmergeRequestId
pullmergeRequestId,
buildId
} = data;
const Dockerfile: Array<string> = [];
const isPnpm = checkPnpm(installCommand, buildCommand, startCommand);
Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
@ -50,8 +51,8 @@ const createDockerfile = async (data, image): Promise<void> => {
export default async function (data) {
try {
const image = 'node:lts';
await createDockerfile(data, image);
const { baseImage, baseBuildImage } = data;
await createDockerfile(data, baseImage);
await buildImage(data);
} catch (error) {
throw error;

View File

@ -11,13 +11,14 @@ const createDockerfile = async (data, image): Promise<void> => {
startCommand,
baseDirectory,
secrets,
pullmergeRequestId
pullmergeRequestId,
buildId
} = data;
const Dockerfile: Array<string> = [];
const isPnpm = checkPnpm(installCommand, buildCommand, startCommand);
Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
@ -49,8 +50,8 @@ const createDockerfile = async (data, image): Promise<void> => {
export default async function (data) {
try {
const image = 'node:lts';
await createDockerfile(data, image);
const { baseImage, baseBuildImage } = data;
await createDockerfile(data, baseImage);
await buildImage(data);
} catch (error) {
throw error;

View File

@ -2,7 +2,7 @@ import { buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
const { workdir, baseDirectory } = data;
const { workdir, baseDirectory, buildId } = data;
const Dockerfile: Array<string> = [];
let composerFound = false;
try {
@ -11,7 +11,7 @@ const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
} catch (error) {}
Dockerfile.push(`FROM ${image}`);
Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`COPY .${baseDirectory || ''} /app`);
if (htaccessFound) {
@ -27,7 +27,7 @@ const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
};
export default async function (data) {
const { workdir, baseDirectory } = data;
const { workdir, baseDirectory, baseImage } = data;
try {
let htaccessFound = false;
try {
@ -36,10 +36,7 @@ export default async function (data) {
} catch (e) {
//
}
const image = htaccessFound
? 'webdevops/php-apache:8.0-alpine'
: 'webdevops/php-nginx:8.0-alpine';
await createDockerfile(data, image, htaccessFound);
await createDockerfile(data, baseImage, htaccessFound);
await buildImage(data);
} catch (error) {
throw error;

View File

@ -10,12 +10,13 @@ const createDockerfile = async (data, image): Promise<void> => {
pullmergeRequestId,
pythonWSGI,
pythonModule,
pythonVariable
pythonVariable,
buildId
} = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
@ -62,8 +63,8 @@ const createDockerfile = async (data, image): Promise<void> => {
export default async function (data) {
try {
const image = 'python:3-alpine';
await createDockerfile(data, image);
const { baseImage, baseBuildImage } = data;
await createDockerfile(data, baseImage);
await buildImage(data);
} catch (error) {
throw error;

View File

@ -2,24 +2,25 @@ import { buildCacheImageWithNode, buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => {
const { applicationId, tag, workdir, publishDirectory } = data;
const { applicationId, tag, workdir, publishDirectory, baseImage, buildId } = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`);
Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
if (baseImage.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
}
Dockerfile.push(`EXPOSE 80`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
};
export default async function (data) {
try {
const image = 'webdevops/nginx:alpine';
const imageForBuild = 'node:lts';
await buildCacheImageWithNode(data, imageForBuild);
await createDockerfile(data, image);
const { baseImage, baseBuildImage } = data;
await buildCacheImageWithNode(data, baseBuildImage);
await createDockerfile(data, baseImage);
await buildImage(data);
} catch (error) {
throw error;

View File

@ -4,11 +4,11 @@ import { promises as fs } from 'fs';
import TOML from '@iarna/toml';
const createDockerfile = async (data, image, name): Promise<void> => {
const { workdir, port, applicationId, tag } = data;
const { workdir, port, applicationId, tag, buildId } = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/target target`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/local/cargo /usr/local/cargo`);
Dockerfile.push(`COPY . .`);
@ -27,14 +27,12 @@ const createDockerfile = async (data, image, name): Promise<void> => {
export default async function (data) {
try {
const { workdir } = data;
const image = 'rust:latest';
const imageForBuild = 'rust:latest';
const { workdir, baseImage, baseBuildImage } = data;
const { stdout: cargoToml } = await asyncExecShell(`cat ${workdir}/Cargo.toml`);
const parsedToml: any = TOML.parse(cargoToml);
const name = parsedToml.package.name;
await buildCacheImageWithCargo(data, imageForBuild);
await createDockerfile(data, image, name);
await buildCacheImageWithCargo(data, baseBuildImage);
await createDockerfile(data, baseImage, name);
await buildImage(data);
} catch (error) {
throw error;

View File

@ -10,13 +10,15 @@ const createDockerfile = async (data, image): Promise<void> => {
baseDirectory,
publishDirectory,
secrets,
pullmergeRequestId
pullmergeRequestId,
baseImage,
buildId
} = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
@ -37,17 +39,18 @@ const createDockerfile = async (data, image): Promise<void> => {
} else {
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
}
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
if (baseImage.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
}
Dockerfile.push(`EXPOSE 80`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
};
export default async function (data) {
try {
const image = 'webdevops/nginx:alpine';
const imageForBuild = 'node:lts';
if (data.buildCommand) await buildCacheImageWithNode(data, imageForBuild);
await createDockerfile(data, image);
const { baseImage, baseBuildImage } = data;
if (data.buildCommand) await buildCacheImageWithNode(data, baseBuildImage);
await createDockerfile(data, baseImage);
await buildImage(data);
} catch (error) {
throw error;

View File

@ -2,25 +2,25 @@ import { buildCacheImageWithNode, buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => {
const { applicationId, tag, workdir, publishDirectory } = data;
const { applicationId, tag, workdir, publishDirectory, baseImage, buildId } = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
if (baseImage.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
}
Dockerfile.push(`EXPOSE 80`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
};
export default async function (data) {
try {
const image = 'webdevops/nginx:alpine';
const imageForBuild = 'node:lts';
await buildCacheImageWithNode(data, imageForBuild);
await createDockerfile(data, image);
const { baseImage, baseBuildImage } = data;
await buildCacheImageWithNode(data, baseBuildImage);
await createDockerfile(data, baseImage);
await buildImage(data);
} catch (error) {
throw error;

View File

@ -2,24 +2,25 @@ import { buildCacheImageWithNode, buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => {
const { applicationId, tag, workdir, publishDirectory } = data;
const { applicationId, tag, workdir, publishDirectory, baseImage, buildId } = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
if (baseImage.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
}
Dockerfile.push(`EXPOSE 80`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
};
export default async function (data) {
try {
const image = 'webdevops/nginx:alpine';
const imageForBuild = 'node:lts';
await buildCacheImageWithNode(data, imageForBuild);
await createDockerfile(data, image);
const { baseImage, baseBuildImage } = data;
await buildCacheImageWithNode(data, baseBuildImage);
await createDockerfile(data, baseImage);
await buildImage(data);
} catch (error) {
throw error;

View File

@ -1,6 +1,7 @@
<script>
export let service;
import Ghost from './svg/services/Ghost.svelte';
import Hasura from './svg/services/Hasura.svelte';
import LanguageTool from './svg/services/LanguageTool.svelte';
import MinIo from './svg/services/MinIO.svelte';
import N8n from './svg/services/N8n.svelte';
@ -11,6 +12,7 @@
import VaultWarden from './svg/services/VaultWarden.svelte';
import VsCodeServer from './svg/services/VSCodeServer.svelte';
import Wordpress from './svg/services/Wordpress.svelte';
import Fider from './svg/services/Fider.svelte';
</script>
{#if service.type === 'plausibleanalytics'}
@ -57,4 +59,12 @@
<a href="https://umami.is" target="_blank">
<Umami />
</a>
{:else if service.type === 'hasura'}
<a href="https://hasura.io" target="_blank">
<Hasura />
</a>
{:else if service.type === 'fider'}
<a href="https://fider.io" target="_blank">
<Fider />
</a>
{/if}

View File

@ -19,7 +19,7 @@ export const staticDeployments = [
'astro',
'eleventy'
];
export const notNodeDeployments = ['php', 'docker', 'rust', 'python', 'deno'];
export const notNodeDeployments = ['php', 'docker', 'rust', 'python', 'deno', 'laravel'];
export function getDomain(domain) {
return domain?.replace('https://', '').replace('http://', '');
@ -191,5 +191,27 @@ export const supportedServiceTypesAndVersions = [
ports: {
main: 3000
}
},
{
name: 'hasura',
fancyName: 'Hasura',
baseImage: 'hasura/graphql-engine',
images: ['postgres:12-alpine'],
versions: ['latest', 'v2.5.1'],
recommendedVersion: 'v2.5.1',
ports: {
main: 8080
}
},
{
name: 'fider',
fancyName: 'Fider',
baseImage: 'getfider/fider',
images: ['postgres:12-alpine'],
versions: ['stable'],
recommendedVersion: 'stable',
ports: {
main: 3000
}
}
];

View File

@ -0,0 +1,10 @@
<svg
class="absolute top-0 left-0 -m-4 h-10 w-10"
viewBox="0 0 50 52"
xmlns="http://www.w3.org/2000/svg"
><title>Logomark</title><path
d="M49.626 11.564a.809.809 0 0 1 .028.209v10.972a.8.8 0 0 1-.402.694l-9.209 5.302V39.25c0 .286-.152.55-.4.694L20.42 51.01c-.044.025-.092.041-.14.058-.018.006-.035.017-.054.022a.805.805 0 0 1-.41 0c-.022-.006-.042-.018-.063-.026-.044-.016-.09-.03-.132-.054L.402 39.944A.801.801 0 0 1 0 39.25V6.334c0-.072.01-.142.028-.21.006-.023.02-.044.028-.067.015-.042.029-.085.051-.124.015-.026.037-.047.055-.071.023-.032.044-.065.071-.093.023-.023.053-.04.079-.06.029-.024.055-.05.088-.069h.001l9.61-5.533a.802.802 0 0 1 .8 0l9.61 5.533h.002c.032.02.059.045.088.068.026.02.055.038.078.06.028.029.048.062.072.094.017.024.04.045.054.071.023.04.036.082.052.124.008.023.022.044.028.068a.809.809 0 0 1 .028.209v20.559l8.008-4.611v-10.51c0-.07.01-.141.028-.208.007-.024.02-.045.028-.068.016-.042.03-.085.052-.124.015-.026.037-.047.054-.071.024-.032.044-.065.072-.093.023-.023.052-.04.078-.06.03-.024.056-.05.088-.069h.001l9.611-5.533a.801.801 0 0 1 .8 0l9.61 5.533c.034.02.06.045.09.068.025.02.054.038.077.06.028.029.048.062.072.094.018.024.04.045.054.071.023.039.036.082.052.124.009.023.022.044.028.068zm-1.574 10.718v-9.124l-3.363 1.936-4.646 2.675v9.124l8.01-4.611zm-9.61 16.505v-9.13l-4.57 2.61-13.05 7.448v9.216l17.62-10.144zM1.602 7.719v31.068L19.22 48.93v-9.214l-9.204-5.209-.003-.002-.004-.002c-.031-.018-.057-.044-.086-.066-.025-.02-.054-.036-.076-.058l-.002-.003c-.026-.025-.044-.056-.066-.084-.02-.027-.044-.05-.06-.078l-.001-.003c-.018-.03-.029-.066-.042-.1-.013-.03-.03-.058-.038-.09v-.001c-.01-.038-.012-.078-.016-.117-.004-.03-.012-.06-.012-.09v-.002-21.481L4.965 9.654 1.602 7.72zm8.81-5.994L2.405 6.334l8.005 4.609 8.006-4.61-8.006-4.608zm4.164 28.764l4.645-2.674V7.719l-3.363 1.936-4.646 2.675v20.096l3.364-1.937zM39.243 7.164l-8.006 4.609 8.006 4.609 8.005-4.61-8.005-4.608zm-.801 10.605l-4.646-2.675-3.363-1.936v9.124l4.645 2.674 3.364 1.937v-9.124zM20.02 38.33l11.743-6.704 5.87-3.35-8-4.606-9.211 5.303-8.395 4.833 7.993 4.524z"
fill="#FF2D20"
fill-rule="evenodd"
/></svg
>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -4,9 +4,7 @@
<svg
class={isAbsolute ? 'absolute top-0 left-0 -m-5 h-10 w-10' : 'mx-auto w-8 h-8'}
height="64"
viewBox="0 0 32 32"
width="64"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
><defs

View File

@ -0,0 +1,121 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<svg
viewBox="0 0 700 240"
xmlns="http://www.w3.org/2000/svg"
class={isAbsolute ? 'w-36 absolute top-0 left-0 -m-3 -mt-5' : 'w-28 mx-auto'}
><path fill="#FDBC3D" d="m90.694 107.498-.981.39-20.608 8.23 6.332 6.547z" /><path
fill="#8EC63F"
d="M61.139 77.914 46.632 93 56.9 103.547c8.649-7.169 17.832-10.502 18.653-10.789L61.139 77.914z"
/><path fill="#208ECB" d="M61.139 77.914 46.367 63.247l-14.228 14.8 14.493 14.952z" /><path
fill="#273C8B"
d="m40.767 57.48-6.943 2.79a38.381 38.381 0 0 0-11.742 7.418L32.14 78.047l14.228-14.8-5.601-5.768z"
/><path
fill="#EE4649"
d="m119.074 138.128-.243-.25-5.653 5.675c1.897-1.516 4.287-3.66 5.896-5.425z"
/><path
fill="#F6944E"
d="m102.088 150.087 3.709-1.875a46.26 46.26 0 0 0 7.381-4.659l5.653-5.676-14.311-15.285-14.493 15.072 12.061 12.423z"
/><path fill="#FFC951" d="m90.279 107.926-14.842 14.74 14.589 14.998 14.493-15.072z" /><path
fill="#F6CC18"
d="m69.087 116.125-11.256 4.493c-3.301.973-6.096 2.843-8.434 5.081l11.548 11.892 14.493-14.926-6.35-6.54z"
/><path
fill="#C5D82D"
d="m56.886 103.559-10.253-10.56L32 107.926l11.784 11.991c3.304-6.888 8.174-12.272 13.103-16.358z"
/><path fill="#0D77B3" d="m32.14 78.047-14.507 14.94 14.365 14.939 14.634-14.927z" /><path
fill="#2A377E"
d="M32.14 78.047 22.08 67.688a38.573 38.573 0 0 0-11.093 18.455l6.645 6.843 14.506-14.94z"
/><path
fill="#DA2128"
d="m94.826 162.454-4.87 5.017 14.808 15.397c-.632-1.942-1.606-4.438-2.58-6.307l-7.357-14.107z"
/><path
fill="#F8A561"
d="m91.24 155.575 10.832-5.48-12.046-12.43-14.506 14.939 14.436 14.867 4.87-5.017z"
/><path fill="#FDBC3D" d="m75.437 122.665-14.493 14.926 14.576 15.013 14.506-14.94z" /><path
fill="#FAD412"
d="M49.397 125.7c-6.71 6.472-9.664 16.047-9.664 16.047-.3-4.606.06-8.83.907-12.698l-8.513 8.742 14.311 14.74 14.506-14.94-11.547-11.892z"
/><path
fill="#C4D52D"
d="m43.783 119.917-11.785-11.991-13.29 13.687 3.708 6.178 9.71 10 8.52-8.775a42.699 42.699 0 0 1 3.137-9.099z"
/><path
fill="#1B80C1"
d="m17.633 92.986-7.638 7.72c.65 5.1 2.35 10.3 5.193 15.04l3.52 5.867 13.29-13.687-14.365-14.94z"
/><path
fill="#1A4685"
d="M10.989 86.143c-1.22 4.667-1.597 9.683-.993 14.563l7.638-7.72-6.645-6.843z"
/><path
fill="#B12026"
d="m89.956 197.35 12.502 13.022c4.143-8.355 5.148-18.255 2.307-27.504l-.302-.311-14.507 14.793z"
/><path fill="#E42028" d="M89.956 167.47 75.52 182.484l14.436 14.867 14.506-14.793z" /><path
fill="#F16B4E"
d="m75.52 152.604-14.576 14.867 14.576 15.012 14.436-15.012z"
/><path fill="#FAD412" d="m60.944 137.591-14.506 14.94 14.506 14.94 14.576-14.867z" /><path
fill="#FFC951"
d="m32.127 137.792-2.293 2.36 10.933 18.22 5.671-5.841z"
/><path fill="#FFC951" d="m22.416 127.79 7.418 12.363 2.293-2.361z" /><path
fill="#981C20"
d="M102.458 210.371 89.955 197.35 75.45 212.29l12.918 13.304a36.951 36.951 0 0 0 14.09-15.222z"
/><path
fill="#C92039"
d="m75.52 182.483-12.59 12.823 6.423 10.704 6.097 6.28 14.506-14.94z"
/><path fill="#F05B41" d="m60.944 167.47-9.096 9.369 11.081 18.467 12.59-12.823z" /><path
fill="#F6CC18"
d="m46.438 152.53-5.671 5.842 11.081 18.467 9.096-9.368z"
/><path
fill="#7A1319"
d="m74.01 213.772 8.904 14.838 4.104-2.237c.429-.233.934-.533 1.35-.78L75.45 212.29l-1.44 1.482z"
/><path fill="#981C20" d="m69.353 206.01 4.658 7.762 1.44-1.482z" /><path
fill="#15796E"
d="m147.842 48.094 10.653-10.971a41.81 41.81 0 0 0 .943-6.94l-11.414-11.755-14.48 14.94 14.298 14.726z"
/><path fill="#29B364" d="m133.53 33.354 14.494-14.926-2.737-2.965-20.95 8.422z" /><path
fill="#21A29F"
d="M151.819 52.189c3.057-4.334 5.434-9.932 6.677-15.066l-10.653 10.971 3.976 4.095z"
/><path
fill="#12827F"
d="M159.438 30.183c.307-6.28-.783-12.862-3.488-19.006l-1.41.567-6.516 6.684 11.414 11.755zM154.54 11.744l-9.253 3.72 2.737 2.964z"
/><path fill="#0C6355" d="m133.336 63.034 14.506-14.94-14.311-14.713-14.493 14.926z" /><path
fill="#1B974D"
d="m104.532 33.368 14.506 14.94 14.48-14.94-9.2-9.476-17.363 6.98z"
/><path fill="#16669F" d="m106.955 30.872-3.485 1.401 1.062 1.095z" /><path
fill="#44BFBD"
d="M135.9 65.674A41.696 41.696 0 0 0 151.82 52.19l-3.977-4.095-14.506 14.94 2.564 2.64z"
/><path
fill="#0D5650"
d="m115.71 74.76 11.052-4.956 6.574-6.77-14.298-14.727-14.506 14.94z"
/><path fill="#3FAF49" d="m119.038 48.307-14.506-14.94-14.576 14.868 14.563 14.999z" /><path
fill="#0D77B3"
d="m104.532 33.368-1.062-1.095-20.97 8.43 7.456 7.532z"
/><path
fill="#0C6355"
d="M134.766 66.217c.352-.157.789-.376 1.134-.543l-2.564-2.64-6.574 6.77 8.004-3.587z"
/><path fill="#12827F" d="m115.71 74.76-11.178-11.513-14.506 14.94 5.47 5.633z" /><path
fill="#4EB648"
d="M104.532 63.247 89.956 48.235 75.52 63.247l14.493 14.927z"
/><path fill="#16669F" d="M89.956 48.235 82.5 40.703l-20.868 8.388L75.52 63.247z" /><path
fill="#FBB139"
d="M129.526 119.012c1.902-7.144 2.108-15.019.353-22.538l-11.048 11.379 10.695 11.16z"
/><path
fill="#E2B523"
d="m110.62 99.542 8.21 8.311 11.049-11.38a46.303 46.303 0 0 0-1.186-4.149l-18.074 7.218z"
/><path fill="#189590" d="M90.026 78.186 76.128 92.501l19.367-8.681z" /><path
fill="#8EC63F"
d="m76.083 92.521 13.943-14.335-14.506-14.94-14.381 14.668 14.413 14.844z"
/><path
fill="#0D77B3"
d="M75.52 63.247 61.633 49.09l-2.264.91-13.002 13.246L61.14 77.914z"
/><path fill="#1953A2" d="m59.37 50.002-18.603 7.477 5.6 5.768z" /><path
fill="#ED3551"
d="M119.324 137.84c.885-.988 2.15-2.59 2.942-3.646l-3.17 3.41.228.236z"
/><path
fill="#F8A561"
d="m118.83 137.877 3.437-3.683a46.268 46.268 0 0 0 7.259-15.182l-10.695-11.159-14.311 14.74 14.31 15.284z"
/><path
fill="#E9B520"
d="m90.279 107.926 14.24 14.666 14.312-14.739-8.212-8.311-19.925 7.956z"
/><path
fill="#EE4649"
d="m118.83 137.877.244.251c.085-.095.166-.193.25-.288l-.228-.235-.265.272z"
/></svg
>

View File

@ -0,0 +1,26 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<svg
class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 mx-auto'}
viewBox="0 0 81 84"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_5273_21928)">
<path
d="M79.7186 28.6019C82.1218 21.073 80.6778 6.03601 76.0158 0.487861C75.4073 -0.238064 74.2624 -0.134361 73.757 0.664158L68.0121 9.72786C66.5887 11.5427 64.0308 11.9575 62.1124 10.6923C55.8827 6.59601 48.4359 4.21082 40.4322 4.21082C32.4285 4.21082 24.9817 6.59601 18.752 10.6923C16.8336 11.9575 14.2757 11.5323 12.8523 9.72786L7.10738 0.664158C6.60199 -0.134361 5.45712 -0.238064 4.84859 0.487861C0.186621 6.03601 -1.25735 21.073 1.14583 28.6019C1.94002 31.1012 2.16693 33.7456 1.69248 36.3279C1.22834 38.879 0.753897 41.9693 0.753897 44.1056C0.753897 66.1323 18.5251 84.0004 40.4322 84.0004C62.3497 84.0004 80.1105 66.1427 80.1105 44.1056C80.1105 41.959 79.6464 38.879 79.1719 36.3279C78.6975 33.7456 78.9244 31.1012 79.7186 28.6019ZM40.4322 75.0819C23.4965 75.0819 9.71684 61.2271 9.71684 44.199C9.71684 43.639 9.73747 43.0893 9.7581 42.5397C10.3769 30.9353 17.3802 21.0108 27.3024 16.2819C31.2836 14.3738 35.7393 13.316 40.4322 13.316C45.1251 13.316 49.5808 14.3842 53.5724 16.2923C63.4945 21.0212 70.4978 30.9456 71.1166 42.5397C71.1476 43.0893 71.1579 43.639 71.1579 44.199C71.1476 61.2271 57.3679 75.0819 40.4322 75.0819Z"
fill="#1EB4D4"
/>
<path
d="M53.7371 56.083L45.8881 42.4044L39.153 30.997C38.9983 30.7274 38.7095 30.5615 38.3898 30.5615H31.9538C31.634 30.5615 31.3452 30.7378 31.1905 31.0074C31.0358 31.2874 31.0358 31.6296 31.2008 31.8993L37.6368 42.7881L28.9936 56.0415C28.8183 56.3111 28.7977 56.6637 28.9524 56.9541C29.1071 57.2444 29.4062 57.4207 29.7259 57.4207H36.2032C36.5023 57.4207 36.7808 57.2652 36.9458 57.0163L41.6181 49.6741L45.8056 56.9748C45.9603 57.2548 46.2594 57.4207 46.5688 57.4207H52.9533C53.273 57.4207 53.5618 57.2548 53.7165 56.9748C53.9022 56.6948 53.9022 56.363 53.7371 56.083Z"
fill="#1EB4D4"
/>
</g>
<defs>
<clipPath id="clip0_5273_21928">
<rect width="81" height="84" fill="white" />
</clipPath>
</defs>
</svg>

View File

@ -5,8 +5,6 @@
<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'}

View File

@ -15,7 +15,6 @@ export function findBuildPack(pack, packageManager = 'npm') {
...metaData,
...defaultBuildAndDeploy(packageManager),
buildCommand: null,
startCommand: null,
publishDirectory: null,
port: null
};
@ -163,6 +162,16 @@ export function findBuildPack(pack, packageManager = 'npm') {
port: 8000
};
}
if (pack === 'laravel') {
return {
...metaData,
installCommand: null,
buildCommand: null,
startCommand: null,
publishDirectory: null,
port: 80
};
}
return {
name: 'node',
fancyName: 'Node.js',
@ -188,18 +197,25 @@ export const buildPacks = [
hoverColor: 'hover:bg-orange-700',
color: 'bg-orange-700'
},
{
name: 'docker',
fancyName: 'Docker',
hoverColor: 'hover:bg-sky-700',
color: 'bg-sky-700'
},
{
name: 'php',
fancyName: 'PHP',
hoverColor: 'hover:bg-indigo-700',
color: 'bg-indigo-700'
},
{
name: 'laravel',
fancyName: 'Laravel',
hoverColor: 'hover:bg-indigo-700',
color: 'bg-indigo-700'
},
{
name: 'docker',
fancyName: 'Docker',
hoverColor: 'hover:bg-sky-700',
color: 'bg-sky-700'
},
{
name: 'svelte',
fancyName: 'Svelte',

View File

@ -12,6 +12,7 @@ import type {
Application,
ApplicationPersistentStorage
} from '@prisma/client';
import { setDefaultBaseImage } from '$lib/buildPacks/common';
export async function listApplications(teamId: string): Promise<Application[]> {
if (teamId === '0') {
@ -195,8 +196,18 @@ export async function getApplication({ id, teamId }: { id: string; teamId: strin
return s;
});
}
const { baseImage, baseBuildImage, baseBuildImages, baseImages } = setDefaultBaseImage(
body.buildPack
);
return { ...body };
// Set default build images
if (!body.baseImage) {
body.baseImage = baseImage;
}
if (!body.baseBuildImage) {
body.baseBuildImage = baseBuildImage;
}
return { ...body, baseBuildImages, baseImages };
}
export async function configureGitRepository({
@ -266,7 +277,9 @@ export async function configureApplication({
pythonVariable,
dockerFileLocation,
denoMainFile,
denoOptions
denoOptions,
baseImage,
baseBuildImage
}: {
id: string;
buildPack: string;
@ -284,6 +297,8 @@ export async function configureApplication({
dockerFileLocation: string;
denoMainFile: string;
denoOptions: string;
baseImage: string;
baseBuildImage: string;
}): Promise<Application> {
return await prisma.application.update({
where: { id },
@ -302,7 +317,9 @@ export async function configureApplication({
pythonVariable,
dockerFileLocation,
denoMainFile,
denoOptions
denoOptions,
baseImage,
baseBuildImage
}
});
}

View File

@ -11,11 +11,12 @@ import generator from 'generate-password';
import forge from 'node-forge';
import getPort, { portNumbers } from 'get-port';
export function generatePassword(length = 24): string {
export function generatePassword(length = 24, symbols = false): string {
return generator.generate({
length,
numbers: true,
strict: true
strict: true,
symbols
});
}

View File

@ -14,7 +14,9 @@ const include: Prisma.ServiceInclude = {
wordpress: true,
ghost: true,
meiliSearch: true,
umami: true
umami: true,
hasura: true,
fider: true
};
export async function listServicesWithIncludes() {
return await prisma.service.findMany({
@ -97,6 +99,17 @@ export async function getService({ id, teamId }: { id: string; teamId: string })
body.umami.umamiAdminPassword = decrypt(body.umami.umamiAdminPassword);
if (body.umami?.hashSalt) body.umami.hashSalt = decrypt(body.umami.hashSalt);
if (body.hasura?.postgresqlPassword)
body.hasura.postgresqlPassword = decrypt(body.hasura.postgresqlPassword);
if (body.hasura?.graphQLAdminPassword)
body.hasura.graphQLAdminPassword = decrypt(body.hasura.graphQLAdminPassword);
if (body.fider?.postgresqlPassword)
body.fider.postgresqlPassword = decrypt(body.fider.postgresqlPassword);
if (body.fider?.jwtSecret) body.fider.jwtSecret = decrypt(body.fider.jwtSecret);
if (body.fider?.emailSmtpPassword)
body.fider.emailSmtpPassword = decrypt(body.fider.emailSmtpPassword);
const settings = await prisma.setting.findFirst();
return { ...body, settings };
@ -243,6 +256,44 @@ export async function configureServiceType({
}
}
});
} else if (type === 'hasura') {
const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword());
const postgresqlDatabase = 'hasura';
const graphQLAdminPassword = encrypt(generatePassword());
await prisma.service.update({
where: { id },
data: {
type,
hasura: {
create: {
postgresqlDatabase,
postgresqlPassword,
postgresqlUser,
graphQLAdminPassword
}
}
}
});
} else if (type === 'fider') {
const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword());
const postgresqlDatabase = 'fider';
const jwtSecret = encrypt(generatePassword(64, true));
await prisma.service.update({
where: { id },
data: {
type,
fider: {
create: {
postgresqlDatabase,
postgresqlPassword,
postgresqlUser,
jwtSecret
}
}
}
});
}
}
@ -301,52 +352,53 @@ export async function updateService({
return await prisma.service.update({ where: { id }, data: { fqdn, name } });
}
export async function updateLanguageToolService({
export async function updateFiderService({
id,
fqdn,
name
name,
emailNoreply,
emailMailgunApiKey,
emailMailgunDomain,
emailMailgunRegion,
emailSmtpHost,
emailSmtpPort,
emailSmtpUser,
emailSmtpPassword,
emailSmtpEnableStartTls
}: {
id: string;
fqdn: string;
name: string;
emailNoreply: string;
emailMailgunApiKey: string;
emailMailgunDomain: string;
emailMailgunRegion: string;
emailSmtpHost: string;
emailSmtpPort: number;
emailSmtpUser: string;
emailSmtpPassword: string;
emailSmtpEnableStartTls: boolean;
}): 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 } });
return await prisma.service.update({
where: { id },
data: {
fqdn,
name,
fider: {
update: {
emailNoreply,
emailMailgunApiKey,
emailMailgunDomain,
emailMailgunRegion,
emailSmtpHost,
emailSmtpPort,
emailSmtpUser,
emailSmtpPassword,
emailSmtpEnableStartTls
}
}
}
});
}
export async function updateWordpress({
@ -398,8 +450,10 @@ export async function updateGhostService({
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.fider.deleteMany({ where: { serviceId: id } });
await prisma.ghost.deleteMany({ where: { serviceId: id } });
await prisma.umami.deleteMany({ where: { serviceId: id } });
await prisma.hasura.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

@ -3,6 +3,34 @@ import { promises as fs } from 'fs';
import { checkPnpm } from './buildPacks/common';
import { saveBuildLog } from './common';
export async function buildCacheImageForLaravel(data, imageForBuild) {
const { applicationId, tag, workdir, docker, buildId, debug, secrets, pullmergeRequestId } = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageForBuild}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
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}`);
}
}
}
});
}
Dockerfile.push(`COPY *.json *.mix.js /app/`);
Dockerfile.push(`COPY resources /app/resources`);
Dockerfile.push(`RUN yarn install && yarn production`);
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ applicationId, tag, workdir, docker, buildId, isCache: true, debug });
}
export async function buildCacheImageWithNode(data, imageForBuild) {
const {
applicationId,
@ -21,7 +49,7 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageForBuild}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
@ -41,10 +69,11 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm');
Dockerfile.push('RUN pnpm add -g pnpm');
}
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
if (installCommand) {
Dockerfile.push(`COPY .${baseDirectory || ''}/package.json ./`);
Dockerfile.push(`RUN ${installCommand}`);
}
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
Dockerfile.push(`RUN ${buildCommand}`);
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ applicationId, tag, workdir, docker, buildId, isCache: true, debug });
@ -65,11 +94,13 @@ export async function buildCacheImageWithCargo(data, imageForBuild) {
} = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageForBuild} as planner-${applicationId}`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push('RUN cargo install cargo-chef');
Dockerfile.push('COPY . .');
Dockerfile.push('RUN cargo chef prepare --recipe-path recipe.json');
Dockerfile.push(`FROM ${imageForBuild}`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push('RUN cargo install cargo-chef');
Dockerfile.push(`COPY --from=planner-${applicationId} /app/recipe.json recipe.json`);

View File

@ -95,6 +95,8 @@ backend {{domain}}
{{/isHttps}}
http-request add-header X-Forwarded-Host %[req.hdr(host),lower]
server {{id}} {{id}}:{{port}}
compression algo gzip
compression type text/html text/css text/plain text/xml text/x-component text/javascript application/x-javascript application/javascript application/json application/manifest+json application/vnd.api+json application/xml application/xhtml+xml application/rss+xml application/atom+xml application/vnd.ms-fontobject application/x-font-ttf application/x-font-opentype application/x-font-truetype image/svg+xml image/x-icon image/vnd.microsoft.icon font/ttf font/eot font/otf font/opentype
{{/isRunning}}
{{/applications}}
@ -111,6 +113,8 @@ backend {{domain}}
{{/isHttps}}
http-request add-header X-Forwarded-Host %[req.hdr(host),lower]
server {{id}} {{id}}:{{port}}
compression algo gzip
compression type text/html text/css text/plain text/xml text/x-component text/javascript application/x-javascript application/javascript application/json application/manifest+json application/vnd.api+json application/xml application/xhtml+xml application/rss+xml application/atom+xml application/vnd.ms-fontobject application/x-font-ttf application/x-font-opentype application/x-font-truetype image/svg+xml image/x-icon image/vnd.microsoft.icon font/ttf font/eot font/otf font/opentype
{{/isRunning}}
{{/services}}
@ -126,6 +130,8 @@ backend {{domain}}
{{/isHttps}}
http-request add-header X-Forwarded-Host %[req.hdr(host),lower]
server {{id}} {{id}}:{{port}} check fall 10
compression algo gzip
compression type text/html text/css text/plain text/xml text/x-component text/javascript application/x-javascript application/javascript application/json application/manifest+json application/vnd.api+json application/xml application/xhtml+xml application/rss+xml application/atom+xml application/vnd.ms-fontobject application/x-font-ttf application/x-font-opentype application/x-font-truetype image/svg+xml image/x-icon image/vnd.microsoft.icon font/ttf font/eot font/otf font/opentype
{{/coolify}}
`;

View File

@ -109,6 +109,7 @@ export async function generateSSLCerts(): Promise<void> {
include: { destinationDocker: true, settings: true },
orderBy: { createdAt: 'desc' }
});
const { fqdn, isDNSCheckEnabled } = await db.prisma.setting.findFirst();
for (const application of applications) {
try {
if (application.fqdn && application.destinationDockerId) {
@ -147,7 +148,6 @@ export async function generateSSLCerts(): Promise<void> {
}
}
const services = await listServicesWithIncludes();
for (const service of services) {
try {
if (service.fqdn && service.destinationDockerId) {
@ -171,7 +171,6 @@ export async function generateSSLCerts(): Promise<void> {
console.log(`Error during generateSSLCerts with ${service.fqdn}: ${error}`);
}
}
const { fqdn } = await db.prisma.setting.findFirst();
if (fqdn) {
const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
@ -193,73 +192,99 @@ export async function generateSSLCerts(): Promise<void> {
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`);
} else {
// 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);
if (isDNSCheckEnabled) {
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`);
} else {
// 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 (
certificates.includes(ssl.domain) ||
certificates.includes(ssl.domain.replace('www.', ''))
) {
console.log(`Certificate for ${ssl.domain} already exists`);
} else {
// 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.');
}
}
}
} else {
if (!dev) {
for (const ssl of ssls) {
if (
certificates.includes(ssl.domain) ||
certificates.includes(ssl.domain.replace('www.', ''))
) {
} else {
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 (
certificates.includes(ssl.domain) ||
certificates.includes(ssl.domain.replace('www.', ''))
) {
console.log(`Certificate for ${ssl.domain} already exists`);
} else {
// 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;
}
}
for (const ssl of ssls) {
if (
certificates.includes(ssl.domain) ||
certificates.includes(ssl.domain.replace('www.', ''))
) {
console.log(`Certificate for ${ssl.domain} already exists`);
} else {
console.log('Generating SSL for', ssl.domain);
}
console.log('DNS settings is incorrect for', ssl.domain, 'skipping.');
}
}
}

View File

@ -124,7 +124,7 @@
"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.",
"found_lock_file": "Found lock file for <span class=\"font-bold text-orange-500 px-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",
@ -184,6 +184,10 @@
"git_source": "Git Source",
"git_repository": "Git Repository",
"build_pack": "Build Pack",
"base_image": "Deplyoment Image",
"base_image_explainer": "Image that will be used for the deployment.",
"base_build_image": "Build Image",
"base_build_image_explainer": "Image that will be used during the build process.",
"destination": "Destination",
"application": "Application",
"url_fqdn": "URL (FQDN)",
@ -226,7 +230,8 @@
"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"
"no_databases_found": "No databases found",
"logs": "Database Logs"
},
"destination": {
"delete_destination": "Delete Destination",
@ -291,20 +296,24 @@
"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"
"no_service": "No services found",
"logs": "Service Logs"
},
"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.",
"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.<br><br><span class='text-yellow-500 font-bold'>WARNING:</span> If you change an already set domain, it will brake webhooks and other integrations! You need to manually update them.",
"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."
"auto_update_enabled_explainer": "Enable automatic updates for Coolify. It will be done automatically behind the scenes, if there is no build process running.",
"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-yellow-500'>both DNS entries</span> set in advance.<br><br>Service needs to be restarted.",
"is_dns_check_enabled": "DNS check enabled?",
"is_dns_check_enabled_explainer": "You can disable DNS check before creating SSL certificates.<br><br>Turning it off is useful when Coolify is behind a reverse proxy or tunnel."
},
"team": {
"pending_invitations": "Pending invitations",

View File

@ -47,7 +47,9 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
pythonWSGI,
pythonModule,
pythonVariable,
denoOptions
denoOptions,
baseImage,
baseBuildImage
} = job.data;
let {
branch,
@ -186,7 +188,7 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
//
}
if (!imageFound || deployNeeded) {
await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId);
await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId, baseImage);
if (buildpacks[buildPack])
await buildpacks[buildPack]({
buildId,
@ -217,7 +219,9 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
pythonVariable,
dockerFileLocation,
denoMainFile,
denoOptions
denoOptions,
baseImage,
baseBuildImage
});
else {
await saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId });

View File

@ -4,34 +4,73 @@ export default async function (): Promise<void> {
const destinationDockers = await prisma.destinationDocker.findMany();
const engines = [...new Set(destinationDockers.map(({ engine }) => engine))];
for (const engine of engines) {
let lowDiskSpace = false;
const host = getEngine(engine);
// Cleanup old coolify images
try {
let { stdout: images } = await asyncExecShell(
`DOCKER_HOST=${host} docker images coollabsio/coolify --filter before="coollabsio/coolify:${version}" -q | xargs `
const { stdout } = await asyncExecShell(
`DOCKER_HOST=${host} docker exec coolify sh -c 'df -kPT /'`
);
images = images.trim();
if (images) {
await asyncExecShell(`DOCKER_HOST=${host} docker rmi -f ${images}`);
let lines = stdout.trim().split('\n');
let header = lines[0];
let regex =
/^Filesystem\s+|Type\s+|1024-blocks|\s+Used|\s+Available|\s+Capacity|\s+Mounted on\s*$/g;
const boundaries = [];
let match;
while ((match = regex.exec(header))) {
boundaries.push(match[0].length);
}
boundaries[boundaries.length - 1] = -1;
const data = lines.slice(1).map((line) => {
const cl = boundaries.map((boundary) => {
const column = boundary > 0 ? line.slice(0, boundary) : line;
line = line.slice(boundary);
return column.trim();
});
return {
capacity: Number.parseInt(cl[5], 10) / 100
};
});
if (data.length > 0) {
const { capacity } = data[0];
if (capacity > 0.8) {
lowDiskSpace = true;
}
}
} catch (error) {
//console.log(error);
console.log(error);
}
try {
await asyncExecShell(`DOCKER_HOST=${host} docker container prune -f`);
} catch (error) {
//console.log(error);
}
try {
await asyncExecShell(`DOCKER_HOST=${host} docker image prune -f --filter "until=2h"`);
} catch (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);
console.log(`Is LowDiskSpace detected? ${lowDiskSpace}`);
if (lowDiskSpace) {
// Cleanup old coolify images
try {
let { stdout: images } = await asyncExecShell(
`DOCKER_HOST=${host} docker images coollabsio/coolify --filter before="coollabsio/coolify:${version}" -q | xargs `
);
images = images.trim();
if (images) {
await asyncExecShell(`DOCKER_HOST=${host} docker rmi -f ${images}`);
}
} catch (error) {
//console.log(error);
}
try {
await asyncExecShell(`DOCKER_HOST=${host} docker container prune -f`);
} catch (error) {
//console.log(error);
}
try {
await asyncExecShell(`DOCKER_HOST=${host} docker image prune -f --filter "until=2h"`);
} catch (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);
}
}
}
}

View File

@ -1,4 +1,5 @@
import { writable, type Writable } from 'svelte/store';
import { browser } from '$app/env';
import { writable, type Writable, type Readable, readable } from 'svelte/store';
export const gitTokens: Writable<{ githubToken: string | null; gitlabToken: string | null }> =
writable({
@ -6,3 +7,8 @@ export const gitTokens: Writable<{ githubToken: string | null; gitlabToken: stri
gitlabToken: null
});
export const disabledButton: Writable<boolean> = writable(false);
export const features: Readable<{ latestVersion: string; beta: boolean }> = readable({
beta: browser && window.localStorage.getItem('beta') === 'true',
latestVersion: browser && window.localStorage.getItem('latestVersion')
});

View File

@ -34,6 +34,8 @@ export type BuilderJob = {
persistentStorage: { path: string }[];
pullmergeRequestId?: unknown;
sourceBranch?: string;
baseImage: string;
baseBuildImage: string;
};
// TODO: Add the other build types

View File

@ -25,7 +25,6 @@
if (res.ok) {
return {
props: {
selectedTeamId: session.teamId,
...(await res.json())
}
};
@ -35,9 +34,6 @@
</script>
<script>
export let teams;
export let selectedTeamId;
import '../tailwind.css';
import { SvelteToast, toast } from '@zerodevx/svelte-toast';
import { page, session } from '$app/stores';
@ -45,7 +41,8 @@
import { errorNotification } from '$lib/form';
import { asyncSleep } from '$lib/components/common';
import { del, get, post } from '$lib/api';
import { browser, dev } from '$app/env';
import { dev } from '$app/env';
import { features } from '$lib/store';
let isUpdateAvailable = false;
let updateStatus = {
@ -56,7 +53,7 @@
let latestVersion = 'latest';
onMount(async () => {
if ($session.userId) {
const overrideVersion = browser && window.localStorage.getItem('latestVersion');
const overrideVersion = $features.latestVersion;
try {
await get(`/login.json`);
} catch ({ error }) {
@ -89,17 +86,6 @@
return errorNotification(error);
}
}
async function switchTeam() {
try {
await post(`/dashboard.json?from=${$page.url.pathname}`, {
cookie: 'teamId',
value: selectedTeamId
});
return window.location.reload();
} catch (error) {
return window.location.reload();
}
}
async function update() {
updateStatus.loading = true;
@ -525,21 +511,10 @@
</div>
</nav>
{#if $session.whiteLabeled}
<span class="fixed bottom-0 left-[50px] z-50 m-2 px-4 text-xs text-stone-700"
<span class="fixed bottom-0 left-[50px] z-50 m-2 px-4 text-xs text-stone-700"
>Powered by <a href="https://coolify.io" target="_blank">Coolify</a></span
>
{/if}
<select
class="fixed right-0 bottom-0 z-50 m-2 w-64 bg-opacity-30 p-2 px-4 hover:bg-opacity-100"
bind:value={selectedTeamId}
on:change={switchTeam}
>
<option value="" disabled selected>Switch to a different team...</option>
{#each teams as team}
<option value={team.teamId}>{team.team.name} - {team.permission}</option>
{/each}
</select>
{/if}
<main>
<slot />

View File

@ -394,7 +394,7 @@
>
<div class="border border-coolgray-500 h-8" />
<a
href={!$disabledButton ? `/applications/${id}/logs` : null}
href={!$disabledButton && isRunning ? `/applications/${id}/logs` : null}
sveltekit:prefetch
class="hover:text-sky-500 rounded"
class:text-sky-500={$page.url.pathname === `/applications/${id}/logs`}
@ -402,7 +402,7 @@
>
<button
title={$t('application.logs')}
disabled={$disabledButton}
disabled={$disabledButton || !isRunning}
class="icons bg-transparent tooltip-bottom text-sm"
data-tooltip={$t('application.logs')}
>

View File

@ -0,0 +1,75 @@
import { asyncExecShell, getEngine, removeDestinationDocker, saveBuildLog } from '$lib/common';
import { buildQueue } from '$lib/queues';
import type { RequestHandler } from '@sveltejs/kit';
import * as db from '$lib/database';
export const post: RequestHandler = async (event) => {
const { buildId, applicationId } = await event.request.json();
if (!buildId) {
return {
status: 500,
body: {
message: 'Build ID not found.'
}
};
}
try {
let count = 0;
await new Promise<void>(async (resolve, reject) => {
const job = await buildQueue.getJob(buildId);
const {
destinationDocker: { engine }
} = job.data;
const host = getEngine(engine);
let interval = setInterval(async () => {
const { status } = await db.prisma.build.findUnique({ where: { id: buildId } });
if (status === 'failed') {
clearInterval(interval);
return resolve();
}
if (count > 1200) {
clearInterval(interval);
reject(new Error('Could not cancel build.'));
}
try {
const { stdout: buildContainers } = await asyncExecShell(
`DOCKER_HOST=${host} docker container ls --filter "label=coolify.buildId=${buildId}" --format '{{json .}}'`
);
if (buildContainers) {
const containersArray = buildContainers.trim().split('\n');
for (const container of containersArray) {
const containerObj = JSON.parse(container);
const id = containerObj.ID;
if (!containerObj.Names.startsWith(`${applicationId}`)) {
await removeDestinationDocker({ id, engine });
clearInterval(interval);
await saveBuildLog({
line: 'Canceled by user!',
buildId: job.data.build_id,
applicationId: job.data.id
});
}
}
}
count++;
} catch (error) {}
}, 100);
resolve();
});
return {
status: 200,
body: {
message: 'Build canceled.'
}
};
} catch (error) {
return {
status: 500,
body: {
message: error.message
}
};
}
};

View File

@ -185,7 +185,7 @@
? $t('application.configuration.loading_repositories')
: $t('application.configuration.select_a_repository')}
id="repository"
showIndicator={true}
showIndicator={!loading.repositories}
isWaiting={loading.repositories}
on:select={loadBranches}
items={reposSelectOptions}
@ -202,7 +202,7 @@
? $t('application.configuration.select_a_repository_first')
: $t('application.configuration.select_a_branch')}
isWaiting={loading.branches}
showIndicator={selected.repository}
showIndicator={selected.repository && !loading.branches}
id="branches"
on:select={isBranchAlreadyUsed}
items={branchSelectOptions}

View File

@ -1,7 +1,7 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler } from '$lib/database';
import { ErrorHandler, generatePassword } from '$lib/database';
export const get: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
@ -34,6 +34,30 @@ export const post: RequestHandler = async (event) => {
try {
await db.configureBuildPack({ id, buildPack });
// Generate default secrets
if (buildPack === 'laravel') {
let found = await db.isSecretExists({ id, name: 'APP_ENV', isPRMRSecret: false });
if (!found) {
await db.createSecret({
id,
name: 'APP_ENV',
value: 'production',
isBuildSecret: false,
isPRMRSecret: false
});
}
found = await db.isSecretExists({ id, name: 'APP_KEY', isPRMRSecret: false });
if (!found) {
await db.createSecret({
id,
name: 'APP_KEY',
value: generatePassword(32),
isBuildSecret: false,
isPRMRSecret: false
});
}
}
return { status: 201 };
} catch (error) {
return ErrorHandler(error);

View File

@ -85,13 +85,14 @@
const composerPHP = files.find(
(file) => file.name === 'composer.json' && file.type === 'blob'
);
const laravel = files.find((file) => file.name === 'artisan' && file.type === 'blob');
if (yarnLock) packageManager = 'yarn';
if (pnpmLock) packageManager = 'pnpm';
if (dockerfile) {
foundConfig = findBuildPack('docker', packageManager);
} else if (packageJson) {
} else if (packageJson && !laravel) {
const path = packageJson.path;
const data = await get(
`${apiUrl}/v4/projects/${projectId}/repository/files/${path}/raw?ref=${branch}`,
@ -107,8 +108,10 @@
foundConfig = findBuildPack('python');
} else if (indexHtml) {
foundConfig = findBuildPack('static', packageManager);
} else if (indexPHP || composerPHP) {
} else if ((indexPHP || composerPHP) && !laravel) {
foundConfig = findBuildPack('php');
} else if (laravel) {
foundConfig = findBuildPack('laravel');
} else {
foundConfig = findBuildPack('node', packageManager);
}
@ -134,13 +137,14 @@
const composerPHP = files.find(
(file) => file.name === 'composer.json' && file.type === 'file'
);
const laravel = files.find((file) => file.name === 'artisan' && file.type === 'file');
if (yarnLock) packageManager = 'yarn';
if (pnpmLock) packageManager = 'pnpm';
if (dockerfile) {
foundConfig = findBuildPack('docker', packageManager);
} else if (packageJson) {
} else if (packageJson && !laravel) {
const data = await get(`${packageJson.git_url}`, {
Authorization: `Bearer ${$gitTokens.githubToken}`,
Accept: 'application/vnd.github.v2.raw'
@ -153,8 +157,10 @@
foundConfig = findBuildPack('python');
} else if (indexHtml) {
foundConfig = findBuildPack('static', packageManager);
} else if (indexPHP || composerPHP) {
} else if ((indexPHP || composerPHP) && !laravel) {
foundConfig = findBuildPack('php');
} else if (laravel) {
foundConfig = findBuildPack('laravel');
} else {
foundConfig = findBuildPack('node', packageManager);
}
@ -225,7 +231,7 @@
<div class="max-w-7xl mx-auto flex flex-wrap justify-center">
{#each buildPacks as buildPack}
<div class="p-2">
<BuildPack {buildPack} {scanning} bind:foundConfig />
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
</div>
{/each}
</div>

View File

@ -45,15 +45,23 @@ export const post: RequestHandler = async (event) => {
}
});
if (pullmergeRequestId) {
await buildQueue.add(buildId, {
build_id: buildId,
type: 'manual',
...applicationFound,
sourceBranch: branch,
pullmergeRequestId
});
await buildQueue.add(
buildId,
{
build_id: buildId,
type: 'manual',
...applicationFound,
sourceBranch: branch,
pullmergeRequestId
},
{ jobId: buildId }
);
} else {
await buildQueue.add(buildId, { build_id: buildId, type: 'manual', ...applicationFound });
await buildQueue.add(
buildId,
{ build_id: buildId, type: 'manual', ...applicationFound },
{ jobId: buildId }
);
}
return {
status: 200,

View File

@ -62,7 +62,9 @@ export const post: RequestHandler = async (event) => {
pythonVariable,
dockerFileLocation,
denoMainFile,
denoOptions
denoOptions,
baseImage,
baseBuildImage
} = await event.request.json();
if (port) port = Number(port);
if (denoOptions) denoOptions = denoOptions.trim();
@ -96,6 +98,8 @@ export const post: RequestHandler = async (event) => {
dockerFileLocation,
denoMainFile,
denoOptions,
baseImage,
baseBuildImage,
...defaultConfiguration
});
return { status: 201 };

View File

@ -33,6 +33,8 @@
gitlabApp: Prisma.GitlabApp;
gitSource: Prisma.GitSource;
destinationDocker: Prisma.DestinationDocker;
baseImages: Array<{ value: string; label: string }>;
baseBuildImages: Array<{ value: string; label: string }>;
};
export let isRunning;
import { page, session } from '$app/stores';
@ -71,11 +73,14 @@
label: 'Gunicorn'
}
];
function containerClass() {
if (!$session.isAdmin || isRunning) {
return 'text-white border border-dashed border-coolgray-300 bg-transparent font-thin px-0';
}
}
if (browser && window.location.hostname === 'demo.coolify.io' && !application.fqdn) {
application.fqdn = `http://${cuid()}.demo.coolify.io`;
}
onMount(() => {
domainEl.focus();
});
@ -138,6 +143,14 @@
async function selectWSGI(event) {
application.pythonWSGI = event.detail.value;
}
async function selectBaseImage(event) {
application.baseImage = event.detail.value;
await handleSubmit();
}
async function selectBaseBuildImage(event) {
application.baseBuildImage = event.detail.value;
await handleSubmit();
}
</script>
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
@ -310,6 +323,49 @@
/>
</div>
</div>
<div class="grid grid-cols-2 items-center">
<label for="baseImage" class="text-base font-bold text-stone-100"
>{$t('application.base_image')}</label
>
<div class="custom-select-wrapper">
<Select
isDisabled={!$session.isAdmin || isRunning}
containerClasses={containerClass()}
id="baseImages"
showIndicator={!isRunning}
items={application.baseImages}
on:select={selectBaseImage}
value={application.baseImage}
isClearable={false}
/>
</div>
<Explainer text={$t('application.base_image_explainer')} />
</div>
{#if application.buildCommand || application.buildPack === 'rust' || application.buildPack === 'laravel'}
<div class="grid grid-cols-2 items-center pb-8">
<label for="baseBuildImage" class="text-base font-bold text-stone-100"
>{$t('application.base_build_image')}</label
>
<div class="custom-select-wrapper">
<Select
isDisabled={!$session.isAdmin || isRunning}
containerClasses={containerClass()}
id="baseBuildImages"
showIndicator={!isRunning}
items={application.baseBuildImages}
on:select={selectBaseBuildImage}
value={application.baseBuildImage}
isClearable={false}
/>
</div>
{#if application.buildPack === 'laravel'}
<Explainer text="For building frontend assets with webpack." />
{:else}
<Explainer text={$t('application.base_build_image_explainer')} />
{/if}
</div>
{/if}
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">{$t('application.application')}</div>
@ -476,21 +532,23 @@
/>
</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"
>{$t('forms.base_directory')}</label
>
<Explainer text={$t('application.directory_to_use_explainer')} />
{#if application.buildPack !== 'laravel'}
<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"
>{$t('forms.base_directory')}</label
>
<Explainer text={$t('application.directory_to_use_explainer')} />
</div>
<input
readonly={!$session.isAdmin}
name="baseDirectory"
id="baseDirectory"
bind:value={application.baseDirectory}
placeholder="{$t('forms.default')}: /"
/>
</div>
<input
readonly={!$session.isAdmin}
name="baseDirectory"
id="baseDirectory"
bind:value={application.baseDirectory}
placeholder="{$t('forms.default')}: /"
/>
</div>
{/if}
{#if !notNodeDeployments.includes(application.buildPack)}
<div class="grid grid-cols-2 items-center">
<div class="flex-col">

View File

@ -8,7 +8,7 @@
import Loading from '$lib/components/Loading.svelte';
import LoadingLogs from '../_Loading.svelte';
import { get } from '$lib/api';
import { get, post } from '$lib/api';
import { errorNotification } from '$lib/form';
import { t } from '$lib/translations';
@ -20,6 +20,8 @@
let followingInterval;
let logsEl;
let cancelInprogress = false;
const { id } = $page.params;
const cleanAnsiCodes = (str: string) => str.replace(/\x1B\[(\d+)m/g, '');
@ -67,6 +69,19 @@
return errorNotification(error);
}
}
async function cancelBuild() {
if (cancelInprogress) return;
try {
cancelInprogress = true;
await post(`/applications/${id}/cancel.json`, {
buildId,
applicationId: id
});
} catch (error) {
console.log(error);
return errorNotification(error);
}
}
onDestroy(() => {
clearInterval(streamInterval);
clearInterval(followingInterval);
@ -90,7 +105,7 @@
<div class="flex justify-end sticky top-0 p-2">
<button
on:click={followBuild}
class="bg-transparent"
class="bg-transparent hover:text-green-500 hover:bg-coolgray-500"
data-tooltip="Follow logs"
class:text-green-500={followingBuild}
>
@ -111,7 +126,30 @@
<line x1="16" y1="12" x2="12" y2="16" />
</svg>
</button>
{#if currentStatus === 'running'}
<button
on:click={cancelBuild}
class="bg-transparent hover:text-red-500 hover:bg-coolgray-500"
data-tooltip="Cancel build"
>
<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="12" cy="12" r="9" />
<path d="M10 10l4 4m0 -4l-4 4" />
</svg>
</button>
{/if}
</div>
<div
class="font-mono 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}

View File

@ -98,12 +98,12 @@
}
</script>
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="flex h-20 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">
Application Logs
</div>
<span class="text-xs">{application.name} </span>
<span class="text-xs">{application.name}</span>
</div>
{#if application.fqdn}

View File

@ -22,6 +22,7 @@
import Astro from '$lib/components/svg/applications/Astro.svelte';
import Eleventy from '$lib/components/svg/applications/Eleventy.svelte';
import Deno from '$lib/components/svg/applications/Deno.svelte';
import Laravel from '$lib/components/svg/applications/Laravel.svelte';
async function newApplication() {
const { id } = await post('/applications/new', {});
@ -104,6 +105,8 @@
<Eleventy />
{:else if application.buildPack.toLowerCase() === 'deno'}
<Deno />
{:else if application.buildPack.toLowerCase() === 'laravel'}
<Laravel />
{/if}
{/if}
@ -162,6 +165,8 @@
<Eleventy />
{:else if application.buildPack.toLowerCase() === 'deno'}
<Deno />
{:else if application.buildPack.toLowerCase() === 'laravel'}
<Laravel />
{/if}
{/if}

View File

@ -57,7 +57,7 @@
</script>
<script>
import { session } from '$app/stores';
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';
@ -65,6 +65,8 @@
import { goto } from '$app/navigation';
import { t } from '$lib/translations';
const { id } = $page.params;
export let database;
export let isRunning;
let loading = false;
@ -163,6 +165,75 @@
</button>
{/if}
{/if}
<div class="border border-stone-700 h-8" />
<a
href="/databases/{id}"
sveltekit:prefetch
class="hover:text-yellow-500 rounded"
class:text-yellow-500={$page.url.pathname === `/databases/${id}`}
class:bg-coolgray-500={$page.url.pathname === `/databases/${id}`}
>
<button
title={$t('application.configurations')}
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
data-tooltip={$t('application.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
>
<div class="border border-stone-700 h-8" />
<a
href={isRunning ? `/databases/${id}/logs` : null}
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/databases/${id}/logs`}
class:bg-coolgray-500={$page.url.pathname === `/databases/${id}/logs`}
>
<button
title={$t('database.logs')}
disabled={!isRunning}
class="icons bg-transparent tooltip-bottom text-sm"
data-tooltip={$t('database.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
>
<button
on:click={deleteDatabase}
title={$t('database.delete_database')}

View File

@ -0,0 +1,41 @@
<div class="lds-ripple absolute left-0">
<div />
<div />
</div>
<style>
.lds-ripple {
display: inline-block;
position: relative;
left: -19px;
top: -8px;
width: 40px;
height: 40px;
}
.lds-ripple div {
position: absolute;
border: 4px solid #fff;
opacity: 1;
border-radius: 50%;
animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
.lds-ripple div:nth-child(2) {
animation-delay: -0.5s;
}
@keyframes lds-ripple {
0% {
top: 1px;
left: 1px;
width: 0;
height: 0;
opacity: 1;
}
100% {
top: 0px;
left: 0px;
width: 36px;
height: 36px;
opacity: 0;
}
}
</style>

View File

@ -0,0 +1,66 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import { dayjs } from '$lib/dayjs';
import { dockerInstance } from '$lib/docker';
import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => {
const { status, body } = await getUserDetails(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.database.findUnique({
where: { id },
include: { destinationDocker: true }
});
if (destinationDockerId) {
const docker = dockerInstance({ destinationDocker });
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
}
};
}
} catch (error) {
const { statusCode } = error;
if (statusCode === 404) {
return {
body: {
logs: []
}
};
}
}
}
return {
status: 200,
body: {
message: 'No logs found.'
}
};
} catch (error) {
return ErrorHandler(error);
}
};

View File

@ -0,0 +1,179 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
import { onDestroy, onMount } from 'svelte';
export const load: Load = async ({ fetch, params, url, stuff }) => {
let endpoint = `/databases/${params.id}/logs.json`;
const res = await fetch(endpoint);
if (res.ok) {
return {
props: {
database: stuff.database,
...(await res.json())
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${url}`)
};
};
</script>
<script lang="ts">
export let database;
import { page } from '$app/stores';
import LoadingLogs from './_Loading.svelte';
import { get } from '$lib/api';
import { errorNotification } from '$lib/form';
import { t } from '$lib/translations';
let loadLogsInterval = null;
let logs = [];
let lastLog = null;
let followingInterval;
let followingLogs;
let logsEl;
let position = 0;
const { id } = $page.params;
onMount(async () => {
loadAllLogs();
loadLogsInterval = setInterval(() => {
loadLogs();
}, 1000);
});
onDestroy(() => {
clearInterval(loadLogsInterval);
clearInterval(followingInterval);
});
async function loadAllLogs() {
try {
const data: any = await get(`/databases/${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: any = await get(
`/databases/${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() {
followingLogs = !followingLogs;
if (followingLogs) {
followingInterval = setInterval(() => {
logsEl.scrollTop = logsEl.scrollHeight;
window.scrollTo(0, document.body.scrollHeight);
}, 1000);
} else {
clearInterval(followingInterval);
}
}
</script>
<div class="flex h-20 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">
Database Logs
</div>
<span class="text-xs">{database.name}</span>
</div>
{#if database.fqdn}
<a
href={database.fqdn}
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}
</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">{$t('application.build.waiting_logs')}</div>
{:else}
<div class="relative w-full">
<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={followingLogs}
>
<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="12" cy="12" r="9" />
<line x1="8" y1="12" x2="12" y2="16" />
<line x1="12" y1="8" x2="12" y2="16" />
<line x1="16" y1="12" x2="12" y2="16" />
</svg>
</button>
</div>
<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 pr-14">
{#each logs as log}
{log + '\n'}
{/each}
</div>
</div>
</div>
{/if}
</div>

View File

@ -25,7 +25,7 @@
</script>
<script lang="ts">
import { session } from '$app/stores';
import { page, session } from '$app/stores';
import { get, post } from '$lib/api';
import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast';
@ -81,6 +81,18 @@
return errorNotification(error);
}
}
async function switchTeam(selectedTeamId) {
try {
await post(`/dashboard.json?from=${$page.url.pathname}`, {
cookie: 'teamId',
value: selectedTeamId
});
return window.location.reload();
} catch (error) {
return window.location.reload();
}
}
</script>
<div class="flex space-x-1 p-6 font-bold">
@ -175,20 +187,39 @@
<div class="flex flex-row flex-wrap justify-center px-2 pb-10 md:flex-row">
{#each ownTeams as team}
<a href="/iam/team/{team.id}" class="w-96 p-2 no-underline">
<div
class="box-selection relative"
class:hover:bg-fuchsia-600={team.id !== '0'}
class:hover:bg-red-500={team.id === '0'}
>
<div class="truncate text-center text-xl font-bold">
{team.name}
<div class="box-selection relative">
<div>
<div class="truncate text-center text-xl font-bold">
{team.name}
</div>
<div class="mt-1 text-center text-xs">
{team.permissions?.length} member(s)
</div>
</div>
<div class="truncate text-center font-bold">
{team.id === '0' ? 'root team' : ''}
</div>
<div class:mt-6={team.id !== '0'} class="mt-1 text-center">
{team.permissions?.length} member(s)
<div class="flex items-center justify-center pt-3">
<button
on:click|preventDefault={() => switchTeam(team.id)}
class:bg-fuchsia-600={$session.teamId !== team.id}
class:hover:bg-fuchsia-500={$session.teamId !== team.id}
class:bg-transparent={$session.teamId === team.id}
disabled={$session.teamId === team.id}
><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 17h5l1.67 -2.386m3.66 -5.227l1.67 -2.387h6" />
<path d="M18 4l3 3l-3 3" />
<path d="M3 7h5l7 10h6" />
<path d="M18 20l3 -3l-3 -3" />
</svg></button
>
</div>
</div>
</a>
@ -207,9 +238,6 @@
<div class="truncate text-center text-xl font-bold">
{team.name}
</div>
<div class="truncate text-center font-bold">
{team.id === '0' ? 'root team' : ''}
</div>
<div class="mt-1 text-center">{team.permissions?.length} member(s)</div>
</div>

View File

@ -1,18 +0,0 @@
import type { RequestHandler } from '@sveltejs/kit';
import * as db from '$lib/database';
export const post: RequestHandler = async (event) => {
const data = await event.request.json();
for (const d of data) {
if (d.container_name) {
const { log, container_name: containerId, source } = d;
console.log(log);
// await db.prisma.applicationLogs.create({ data: { log, containerId: containerId.substr(1), source } });
}
}
return {
status: 200,
body: {}
};
};

View File

@ -0,0 +1,184 @@
<script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations';
import Select from 'svelte-select';
export let service;
export let readOnly;
let mailgunRegions = [
{
value: 'EU',
label: 'EU'
},
{
value: 'US',
label: 'US'
}
];
</script>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">Fider</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="jwtSecret">JWT Secret</label>
<CopyPasswordField
name="jwtSecret"
id="jwtSecret"
isPasswordField
value={service.fider.jwtSecret}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailNoreply">Noreply Email</label>
<input
name="emailNoreply"
id="emailNoreply"
type="email"
required
readonly={readOnly}
disabled={readOnly}
bind:value={service.fider.emailNoreply}
placeholder="{$t('forms.eg')}: noreply@yourdomain.com"
/>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">Email</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailMailgunApiKey">Mailgun API Key</label>
<CopyPasswordField
name="emailMailgunApiKey"
id="emailMailgunApiKey"
isPasswordField
bind:value={service.fider.emailMailgunApiKey}
readonly={readOnly}
disabled={readOnly}
placeholder="{$t('forms.eg')}: key-yourkeygoeshere"
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailMailgunDomain">Mailgun Domain</label>
<input
name="emailMailgunDomain"
id="emailMailgunDomain"
readonly={readOnly}
disabled={readOnly}
bind:value={service.fider.emailMailgunDomain}
placeholder="{$t('forms.eg')}: yourdomain.com"
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailMailgunRegion">Mailgun Region</label>
<div class="custom-select-wrapper">
<Select
id="baseBuildImages"
items={mailgunRegions}
showIndicator
on:select={(event) => (service.fider.emailMailgunRegion = event.detail.value)}
value={service.fider.emailMailgunRegion || 'EU'}
isClearable={false}
/>
</div>
</div>
<div class="flex space-x-1 py-5 px-10 font-bold">
<div class="text-lg">Or</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailSmtpHost">SMTP Host</label>
<input
name="emailSmtpHost"
id="emailSmtpHost"
readonly={readOnly}
disabled={readOnly}
bind:value={service.fider.emailSmtpHost}
placeholder="{$t('forms.eg')}: smtp.yourdomain.com"
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailSmtpPort">SMTP Port</label>
<input
name="emailSmtpPort"
id="emailSmtpPort"
readonly={readOnly}
disabled={readOnly}
bind:value={service.fider.emailSmtpPort}
placeholder="{$t('forms.eg')}: 587"
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailSmtpUser">SMTP User</label>
<input
name="emailSmtpUser"
id="emailSmtpUser"
readonly={readOnly}
disabled={readOnly}
bind:value={service.fider.emailSmtpUser}
placeholder="{$t('forms.eg')}: user@yourdomain.com"
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailSmtpPassword">SMTP Password</label>
<CopyPasswordField
name="emailSmtpPassword"
id="emailSmtpPassword"
isPasswordField
value={service.fider.emailSmtpPassword}
readonly={readOnly}
disabled={readOnly}
placeholder="{$t('forms.eg')}: s0m3p4ssw0rd"
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailSmtpEnableStartTls">SMTP Start TLS</label>
<input
name="emailSmtpEnableStartTls"
id="emailSmtpEnableStartTls"
readonly={readOnly}
disabled={readOnly}
bind:value={service.fider.emailSmtpEnableStartTls}
placeholder="{$t('forms.eg')}: true"
/>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">PostgreSQL</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlUser">{$t('forms.username')}</label>
<CopyPasswordField
name="postgresqlUser"
id="postgresqlUser"
value={service.fider.postgresqlUser}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlPassword">{$t('forms.password')}</label>
<CopyPasswordField
id="postgresqlPassword"
isPasswordField
readonly
disabled
name="postgresqlPassword"
value={service.fider.postgresqlPassword}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlDatabase">{$t('index.database')}</label>
<CopyPasswordField
name="postgresqlDatabase"
id="postgresqlDatabase"
value={service.fider.postgresqlDatabase}
readonly
disabled
/>
</div>

View File

@ -0,0 +1,58 @@
<script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations';
export let service;
</script>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">Hasura</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="graphQLAdminPassword">GraphQL Admin Password</label>
<CopyPasswordField
name="graphQLAdminPassword"
id="graphQLAdminPassword"
isPasswordField
value={service.hasura.graphQLAdminPassword}
readonly
disabled
/>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">PostgreSQL</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlUser">{$t('forms.username')}</label>
<CopyPasswordField
name="postgresqlUser"
id="postgresqlUser"
value={service.hasura.postgresqlUser}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlPassword">{$t('forms.password')}</label>
<CopyPasswordField
id="postgresqlPassword"
isPasswordField
readonly
disabled
name="postgresqlPassword"
value={service.hasura.postgresqlPassword}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlDatabase">{$t('index.database')}</label>
<CopyPasswordField
name="postgresqlDatabase"
id="postgresqlDatabase"
value={service.hasura.postgresqlDatabase}
readonly
disabled
/>
</div>

View File

@ -12,7 +12,9 @@
import { errorNotification } from '$lib/form';
import { t } from '$lib/translations';
import { toast } from '@zerodevx/svelte-toast';
import Fider from './_Fider.svelte';
import Ghost from './_Ghost.svelte';
import Hasura from './_Hasura.svelte';
import MeiliSearch from './_MeiliSearch.svelte';
import MinIo from './_MinIO.svelte';
import PlausibleAnalytics from './_PlausibleAnalytics.svelte';
@ -172,6 +174,10 @@
<MeiliSearch bind:service />
{:else if service.type === 'umami'}
<Umami bind:service />
{:else if service.type === 'hasura'}
<Hasura bind:service />
{:else if service.type === 'fider'}
<Fider bind:service {readOnly} />
{/if}
</div>
</form>

View File

@ -270,6 +270,38 @@
</button></a
>
<div class="border border-stone-700 h-8" />
<a
href={isRunning ? `/services/${id}/logs` : null}
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/services/${id}/logs`}
class:bg-coolgray-500={$page.url.pathname === `/services/${id}/logs`}
>
<button
title={$t('service.logs')}
disabled={!isRunning}
class="icons bg-transparent tooltip-bottom text-sm"
data-tooltip={$t('service.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
>
{/if}
<button
on:click={deleteService}

View File

@ -44,6 +44,8 @@
import { t } from '$lib/translations';
import MeiliSearch from '$lib/components/svg/services/MeiliSearch.svelte';
import Umami from '$lib/components/svg/services/Umami.svelte';
import Hasura from '$lib/components/svg/services/Hasura.svelte';
import Fider from '$lib/components/svg/services/Fider.svelte';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
@ -93,6 +95,10 @@
<MeiliSearch isAbsolute />
{:else if type.name === 'umami'}
<Umami isAbsolute />
{:else if type.name === 'hasura'}
<Hasura isAbsolute />
{:else if type.name === 'fider'}
<Fider isAbsolute />
{/if}{type.fancyName}
</button>
</form>

View File

@ -0,0 +1,57 @@
import { getUserDetails } from '$lib/common';
import { encrypt } from '$lib/crypto';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
let {
name,
fqdn,
fider: {
emailNoreply,
emailMailgunApiKey,
emailMailgunDomain,
emailMailgunRegion,
emailSmtpHost,
emailSmtpPort,
emailSmtpUser,
emailSmtpPassword,
emailSmtpEnableStartTls
}
} = await event.request.json();
if (fqdn) fqdn = fqdn.toLowerCase();
if (emailNoreply) emailNoreply = emailNoreply.toLowerCase();
if (emailSmtpHost) emailSmtpHost = emailSmtpHost.toLowerCase();
if (emailSmtpPassword) {
emailSmtpPassword = encrypt(emailSmtpPassword);
}
if (emailSmtpPort) emailSmtpPort = Number(emailSmtpPort);
if (emailSmtpEnableStartTls) emailSmtpEnableStartTls = Boolean(emailSmtpEnableStartTls);
try {
await db.updateFiderService({
id,
fqdn,
name,
emailNoreply,
emailMailgunApiKey,
emailMailgunDomain,
emailMailgunRegion,
emailSmtpHost,
emailSmtpPort,
emailSmtpUser,
emailSmtpPassword,
emailSmtpEnableStartTls
});
return { status: 201 };
} catch (error) {
return ErrorHandler(error);
}
};

View File

@ -0,0 +1,154 @@
import {
asyncExecShell,
createDirectories,
getDomain,
getEngine,
getUserDetails
} from '$lib/common';
import * as db from '$lib/database';
import { promises as fs } from 'fs';
import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile';
import type { Service, DestinationDocker, Prisma } from '@prisma/client';
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
try {
const service: Service & Prisma.ServiceInclude & { destinationDocker: DestinationDocker } =
await db.getService({ id, teamId });
const {
type,
version,
fqdn,
destinationDockerId,
destinationDocker,
serviceSecret,
fider: {
postgresqlUser,
postgresqlPassword,
postgresqlDatabase,
jwtSecret,
emailNoreply,
emailMailgunApiKey,
emailMailgunDomain,
emailMailgunRegion,
emailSmtpHost,
emailSmtpPort,
emailSmtpUser,
emailSmtpPassword,
emailSmtpEnableStartTls
}
} = service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
const domain = getDomain(fqdn);
const config = {
fider: {
image: `${image}:${version}`,
environmentVariables: {
HOST_DOMAIN: domain,
DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}?sslmode=disable`,
JWT_SECRET: `${jwtSecret.replace(/\$/g, '$$$')}`,
EMAIL_NOREPLY: emailNoreply,
EMAIL_MAILGUN_API: emailMailgunApiKey,
EMAIL_MAILGUN_REGION: emailMailgunRegion,
EMAIL_MAILGUN_DOMAIN: emailMailgunDomain,
EMAIL_SMTP_HOST: emailSmtpHost,
EMAIL_SMTP_PORT: emailSmtpPort,
EMAIL_SMTP_USER: emailSmtpUser,
EMAIL_SMTP_PASSWORD: emailSmtpPassword,
EMAIL_SMTP_ENABLE_STARTTLS: emailSmtpEnableStartTls
}
},
postgresql: {
image: 'postgres:12-alpine',
volume: `${id}-postgresql-data:/var/lib/postgresql/data`,
environmentVariables: {
POSTGRES_USER: postgresqlUser,
POSTGRES_PASSWORD: postgresqlPassword,
POSTGRES_DB: postgresqlDatabase
}
}
};
if (serviceSecret.length > 0) {
serviceSecret.forEach((secret) => {
config.fider.environmentVariables[secret.name] = secret.value;
});
}
const composeFile: ComposeFile = {
version: '3.8',
services: {
[id]: {
container_name: id,
image: config.fider.image,
environment: config.fider.environmentVariables,
networks: [network],
volumes: [],
restart: 'always',
labels: makeLabelForServices('fider'),
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
},
depends_on: [`${id}-postgresql`]
},
[`${id}-postgresql`]: {
image: config.postgresql.image,
container_name: `${id}-postgresql`,
environment: config.postgresql.environmentVariables,
networks: [network],
volumes: [config.postgresql.volume],
restart: 'always',
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
}
},
networks: {
[network]: {
external: true
}
},
volumes: {
[config.postgresql.volume.split(':')[0]]: {
name: config.postgresql.volume.split(':')[0]
}
}
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try {
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return {
status: 200
};
} catch (error) {
console.log(error);
return ErrorHandler(error);
}
} catch (error) {
return ErrorHandler(error);
}
};

View File

@ -0,0 +1,42 @@
import { getUserDetails, removeDestinationDocker } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import { checkContainer, stopTcpHttpProxy } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
try {
const service = await db.getService({ id, teamId });
const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
const found = await checkContainer(engine, id);
if (found) {
await removeDestinationDocker({ id, engine });
}
} catch (error) {
console.error(error);
}
try {
const found = await checkContainer(engine, `${id}-postgresql`);
if (found) {
await removeDestinationDocker({ id: `${id}-postgresql`, engine });
}
} catch (error) {
console.error(error);
}
}
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
}
};

View File

@ -0,0 +1,21 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
let { name, fqdn } = await event.request.json();
if (fqdn) fqdn = fqdn.toLowerCase();
try {
await db.updateService({ id, fqdn, name });
return { status: 201 };
} catch (error) {
return ErrorHandler(error);
}
};

View File

@ -0,0 +1,122 @@
import { asyncExecShell, createDirectories, getEngine, getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { promises as fs } from 'fs';
import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile';
import type { Service, DestinationDocker, Prisma } from '@prisma/client';
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
try {
const service: Service & Prisma.ServiceInclude & { destinationDocker: DestinationDocker } =
await db.getService({ id, teamId });
const {
type,
version,
destinationDockerId,
destinationDocker,
serviceSecret,
hasura: { postgresqlUser, postgresqlPassword, postgresqlDatabase }
} = service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
const config = {
hasura: {
image: `${image}:${version}`,
environmentVariables: {
HASURA_GRAPHQL_METADATA_DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}`
}
},
postgresql: {
image: 'postgres:12-alpine',
volume: `${id}-postgresql-data:/var/lib/postgresql/data`,
environmentVariables: {
POSTGRES_USER: postgresqlUser,
POSTGRES_PASSWORD: postgresqlPassword,
POSTGRES_DB: postgresqlDatabase
}
}
};
if (serviceSecret.length > 0) {
serviceSecret.forEach((secret) => {
config.hasura.environmentVariables[secret.name] = secret.value;
});
}
const composeFile: ComposeFile = {
version: '3.8',
services: {
[id]: {
container_name: id,
image: config.hasura.image,
environment: config.hasura.environmentVariables,
networks: [network],
volumes: [],
restart: 'always',
labels: makeLabelForServices('hasura'),
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
},
depends_on: [`${id}-postgresql`]
},
[`${id}-postgresql`]: {
image: config.postgresql.image,
container_name: `${id}-postgresql`,
environment: config.postgresql.environmentVariables,
networks: [network],
volumes: [config.postgresql.volume],
restart: 'always',
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
}
},
networks: {
[network]: {
external: true
}
},
volumes: {
[config.postgresql.volume.split(':')[0]]: {
name: config.postgresql.volume.split(':')[0]
}
}
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try {
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return {
status: 200
};
} catch (error) {
console.log(error);
return ErrorHandler(error);
}
} catch (error) {
return ErrorHandler(error);
}
};

View File

@ -0,0 +1,42 @@
import { getUserDetails, removeDestinationDocker } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import { checkContainer, stopTcpHttpProxy } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
try {
const service = await db.getService({ id, teamId });
const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
const found = await checkContainer(engine, id);
if (found) {
await removeDestinationDocker({ id, engine });
}
} catch (error) {
console.error(error);
}
try {
const found = await checkContainer(engine, `${id}-postgresql`);
if (found) {
await removeDestinationDocker({ id: `${id}-postgresql`, engine });
}
} catch (error) {
console.error(error);
}
}
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
}
};

View File

@ -0,0 +1,41 @@
<div class="lds-ripple absolute left-0">
<div />
<div />
</div>
<style>
.lds-ripple {
display: inline-block;
position: relative;
left: -19px;
top: -8px;
width: 40px;
height: 40px;
}
.lds-ripple div {
position: absolute;
border: 4px solid #fff;
opacity: 1;
border-radius: 50%;
animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
.lds-ripple div:nth-child(2) {
animation-delay: -0.5s;
}
@keyframes lds-ripple {
0% {
top: 1px;
left: 1px;
width: 0;
height: 0;
opacity: 1;
}
100% {
top: 0px;
left: 0px;
width: 36px;
height: 36px;
opacity: 0;
}
}
</style>

View File

@ -0,0 +1,66 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import { dayjs } from '$lib/dayjs';
import { dockerInstance } from '$lib/docker';
import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => {
const { status, body } = await getUserDetails(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.service.findUnique({
where: { id },
include: { destinationDocker: true }
});
if (destinationDockerId) {
const docker = dockerInstance({ destinationDocker });
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
}
};
}
} catch (error) {
const { statusCode } = error;
if (statusCode === 404) {
return {
body: {
logs: []
}
};
}
}
}
return {
status: 200,
body: {
message: 'No logs found.'
}
};
} catch (error) {
return ErrorHandler(error);
}
};

View File

@ -0,0 +1,179 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
import { onDestroy, onMount } from 'svelte';
export const load: Load = async ({ fetch, params, url, stuff }) => {
let endpoint = `/services/${params.id}/logs.json`;
const res = await fetch(endpoint);
if (res.ok) {
return {
props: {
service: stuff.service,
...(await res.json())
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${url}`)
};
};
</script>
<script lang="ts">
export let service;
import { page } from '$app/stores';
import LoadingLogs from './_Loading.svelte';
import { get } from '$lib/api';
import { errorNotification } from '$lib/form';
import { t } from '$lib/translations';
let loadLogsInterval = null;
let logs = [];
let lastLog = null;
let followingInterval;
let followingLogs;
let logsEl;
let position = 0;
const { id } = $page.params;
onMount(async () => {
loadAllLogs();
loadLogsInterval = setInterval(() => {
loadLogs();
}, 1000);
});
onDestroy(() => {
clearInterval(loadLogsInterval);
clearInterval(followingInterval);
});
async function loadAllLogs() {
try {
const data: any = await get(`/services/${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: any = await get(
`/services/${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() {
followingLogs = !followingLogs;
if (followingLogs) {
followingInterval = setInterval(() => {
logsEl.scrollTop = logsEl.scrollHeight;
window.scrollTo(0, document.body.scrollHeight);
}, 1000);
} else {
clearInterval(followingInterval);
}
}
</script>
<div class="flex h-20 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">
Service Logs
</div>
<span class="text-xs">{service.name}</span>
</div>
{#if service.fqdn}
<a
href={service.fqdn}
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}
</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">{$t('application.build.waiting_logs')}</div>
{:else}
<div class="relative w-full">
<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={followingLogs}
>
<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="12" cy="12" r="9" />
<line x1="8" y1="12" x2="12" y2="16" />
<line x1="12" y1="8" x2="12" y2="16" />
<line x1="16" y1="12" x2="12" y2="16" />
</svg>
</button>
</div>
<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 pr-14">
{#each logs as log}
{log + '\n'}
{/each}
</div>
</div>
</div>
{/if}
</div>

View File

@ -16,6 +16,8 @@
import { session } from '$app/stores';
import { getDomain } from '$lib/components/common';
import Umami from '$lib/components/svg/services/Umami.svelte';
import Hasura from '$lib/components/svg/services/Hasura.svelte';
import Fider from '$lib/components/svg/services/Fider.svelte';
export let services;
async function newService() {
@ -89,6 +91,10 @@
<MeiliSearch isAbsolute />
{:else if service.type === 'umami'}
<Umami isAbsolute />
{:else if service.type === 'hasura'}
<Hasura isAbsolute />
{:else if service.type === 'fider'}
<Fider isAbsolute />
{/if}
<div class="truncate text-center text-xl font-bold">
{service.name}
@ -138,6 +144,10 @@
<MeiliSearch isAbsolute />
{:else if service.type === 'umami'}
<Umami isAbsolute />
{:else if service.type === 'hasura'}
<Hasura isAbsolute />
{:else if service.type === 'fider'}
<Fider isAbsolute />
{/if}
<div class="truncate text-center text-xl font-bold">
{service.name}

View File

@ -64,13 +64,20 @@ export const post: RequestHandler = async (event) => {
};
if (status === 401) return { status, body };
const { fqdn, isRegistrationEnabled, dualCerts, minPort, maxPort, isAutoUpdateEnabled } =
await event.request.json();
const {
fqdn,
isRegistrationEnabled,
dualCerts,
minPort,
maxPort,
isAutoUpdateEnabled,
isDNSCheckEnabled
} = await event.request.json();
try {
const { id } = await db.listSettings();
await db.prisma.setting.update({
where: { id },
data: { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled }
data: { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled, isDNSCheckEnabled }
});
if (fqdn) {
await db.prisma.setting.update({ where: { id }, data: { fqdn } });

View File

@ -28,8 +28,6 @@
import { session } from '$app/stores';
export let settings;
import Cookies from 'js-cookie';
import langs from '$lib/lang.json';
import Setting from '$lib/components/Setting.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { errorNotification } from '$lib/form';
@ -39,10 +37,12 @@
import { getDomain } from '$lib/components/common';
import { toast } from '@zerodevx/svelte-toast';
import { t } from '$lib/translations';
import { features } from '$lib/store';
let isRegistrationEnabled = settings.isRegistrationEnabled;
let dualCerts = settings.dualCerts;
let isAutoUpdateEnabled = settings.isAutoUpdateEnabled;
let isDNSCheckEnabled = settings.isDNSCheckEnabled;
let minPort = settings.minPort;
let maxPort = settings.maxPort;
@ -78,7 +78,15 @@
if (name === 'isAutoUpdateEnabled') {
isAutoUpdateEnabled = !isAutoUpdateEnabled;
}
await post(`/settings.json`, { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled });
if (name === 'isDNSCheckEnabled') {
isDNSCheckEnabled = !isDNSCheckEnabled;
}
await post(`/settings.json`, {
isRegistrationEnabled,
dualCerts,
isAutoUpdateEnabled,
isDNSCheckEnabled
});
return toast.push(t.get('application.settings_saved'));
} catch ({ error }) {
return errorNotification(error);
@ -176,13 +184,21 @@
/>
</div>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
bind:setting={isDNSCheckEnabled}
title={$t('setting.is_dns_check_enabled')}
description={$t('setting.is_dns_check_enabled_explainer')}
on:click={() => changeSettings('isDNSCheckEnabled')}
/>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
dataTooltip={$t('setting.must_remove_domain_before_changing')}
disabled={isFqdnSet}
bind:setting={dualCerts}
title={$t('application.ssl_www_and_non_www')}
description={$t('services.generate_www_non_www_ssl')}
description={$t('setting.generate_www_non_www_ssl')}
on:click={() => !isFqdnSet && changeSettings('dualCerts')}
/>
</div>
@ -194,7 +210,7 @@
on:click={() => changeSettings('isRegistrationEnabled')}
/>
</div>
{#if browser && (window.location.hostname === 'staging.coolify.io' || window.location.hostname === 'localhost')}
{#if browser && $features.beta}
<div class="grid grid-cols-2 items-center">
<Setting
bind:setting={isAutoUpdateEnabled}

View File

@ -100,11 +100,15 @@ export const post: RequestHandler = async (event) => {
type: 'webhook_commit'
}
});
await buildQueue.add(buildId, {
build_id: buildId,
type: 'webhook_commit',
...applicationFound
});
await buildQueue.add(
buildId,
{
build_id: buildId,
type: 'webhook_commit',
...applicationFound
},
{ jobId: buildId }
);
return {
status: 200,
body: {
@ -160,13 +164,17 @@ export const post: RequestHandler = async (event) => {
type: 'webhook_pr'
}
});
await buildQueue.add(buildId, {
build_id: buildId,
type: 'webhook_pr',
...applicationFound,
sourceBranch,
pullmergeRequestId
});
await buildQueue.add(
buildId,
{
build_id: buildId,
type: 'webhook_pr',
...applicationFound,
sourceBranch,
pullmergeRequestId
},
{ jobId: buildId }
);
return {
status: 200,
body: {

View File

@ -73,11 +73,15 @@ export const post: RequestHandler = async (event) => {
type: 'webhook_commit'
}
});
await buildQueue.add(buildId, {
build_id: buildId,
type: 'webhook_commit',
...applicationFound
});
await buildQueue.add(
buildId,
{
build_id: buildId,
type: 'webhook_commit',
...applicationFound
},
{ jobId: buildId }
);
return {
status: 200,
body: {
@ -156,13 +160,17 @@ export const post: RequestHandler = async (event) => {
type: 'webhook_mr'
}
});
await buildQueue.add(buildId, {
build_id: buildId,
type: 'webhook_mr',
...applicationFound,
sourceBranch,
pullmergeRequestId
});
await buildQueue.add(
buildId,
{
build_id: buildId,
type: 'webhook_mr',
...applicationFound,
sourceBranch,
pullmergeRequestId
},
{ jobId: buildId }
);
return {
status: 200,
body: {

View File

@ -46,14 +46,14 @@ #svelte .custom-select-wrapper .selectContainer input {
}
#svelte .custom-select-wrapper .selectContainer {
@apply h-12 w-96 rounded border-none bg-coolgray-200 p-2 text-xs font-bold tracking-tight outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 md:text-sm;
@apply h-12 w-96 rounded border-none bg-coolgray-200 p-2 px-0 text-xs tracking-tight outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 md:text-sm;
}
#svelte .listContainer {
@apply bg-coolgray-400 text-white scrollbar-w-2 scrollbar-thumb-green-500 scrollbar-track-coolgray-200;
}
#svelte .selectedItem {
@apply pl-3;
@apply pl-2;
}
#svelte .item.hover {
@ -64,7 +64,7 @@ #svelte .item.active {
}
select {
@apply h-12 w-96 rounded bg-coolgray-200 p-2 text-xs font-bold tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:text-stone-600 md:text-sm;
@apply h-12 w-96 rounded bg-coolgray-200 p-2 text-xs font-bold tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:text-stone-600 md:text-sm;
}
.svelte-select {
--background: rgb(32 32 32);