Compare commits

...

64 Commits

Author SHA1 Message Date
Andras Bacsai
ad3044dce1 Merge pull request #234 from coollabsio/v2.2.0
v2.2.0
2022-03-27 22:48:04 +02:00
Andras Bacsai
e40541d831 design: Colors on svelte-select 2022-03-27 22:40:36 +02:00
Andras Bacsai
2786e7dbaf Merge pull request #235 from SaraVieira/add-better-repo-select
Add search in repo and branch select
2022-03-27 22:34:15 +02:00
Andras Bacsai
196d681a63 fix: Ghost icon, remove console.log 2022-03-27 22:32:31 +02:00
Andras Bacsai
d2353e3c35 fix: Ghost logo size 2022-03-27 22:28:39 +02:00
Andras Bacsai
2475031f88 feat: Ghost service 2022-03-27 22:03:21 +02:00
Sara Vieira
cd15e68adc remove console.log 2022-03-27 21:56:23 +02:00
Sara Vieira
27431f779d clean css 2022-03-27 21:50:28 +02:00
Sara Vieira
b9b5a2faeb add search in repo and branch select 2022-03-27 21:48:25 +02:00
Andras Bacsai
e471b11d3b Add icons 2022-03-27 14:37:32 +02:00
Andras Bacsai
a742a3d2e3 feat: Add update kuma service 2022-03-27 14:05:36 +02:00
Andras Bacsai
c615f6c07e chore: Version ++ 2022-03-27 13:49:13 +02:00
Andras Bacsai
a6ebfb08f7 feat: Add n8n.io service 2022-03-27 13:49:04 +02:00
Andras Bacsai
2b0d162226 Merge pull request #232 from coollabsio/v2.1.1
v2.1.1
2022-03-25 15:41:26 +01:00
Andras Bacsai
2c5f09a8bb fix: Cleanup only 2 hours+ old images 2022-03-25 15:34:14 +01:00
Andras Bacsai
ef073e586b Test nocheck proxy 2022-03-25 10:48:11 +01:00
Andras Bacsai
82bfdb87e3 UI fixes 2022-03-25 10:36:47 +01:00
Andras Bacsai
767e7b80cb chore: version++ 2022-03-25 09:14:55 +01:00
Andras Bacsai
8d26ea9063 Update packages 2022-03-25 09:14:32 +01:00
Andras Bacsai
1a7c4310d0 Merge pull request #230 from coollabsio/v2.1.0
v2.1.0
2022-03-23 11:54:15 +01:00
Andras Bacsai
4e8fe79e2b feat: Be able to redeploy PRs 2022-03-23 11:49:40 +01:00
Andras Bacsai
a8c5551292 fix: Volumes 2022-03-23 11:14:38 +01:00
Andras Bacsai
2bf73109b2 feat: Use compose instead of normal docker cmd 2022-03-23 10:25:32 +01:00
Andras Bacsai
f0ab3750bd Disable PHP modules, as the new image has all activated by default 2022-03-22 15:56:03 +01:00
Andras Bacsai
58a11e37fe Add schema 2022-03-22 14:58:08 +01:00
Andras Bacsai
927bf46304 fix: skip ssl cert in case of error 2022-03-22 10:37:33 +01:00
Andras Bacsai
6b89857697 chore: version++ 2022-03-22 10:24:52 +01:00
Andras Bacsai
b72e5ccef6 Merge branch 'main' into v2.0.32 2022-03-22 10:23:11 +01:00
Andras Bacsai
6617b7811b log ssl errors 2022-03-22 10:22:20 +01:00
Andras Bacsai
e1c1988db4 Merge pull request #229 from coollabsio/importer-error
Add debug for GH importer
2022-03-22 09:42:49 +01:00
Andras Bacsai
af99ea4678 Add debug for GH importer 2022-03-22 09:35:24 +01:00
Andras Bacsai
a6d5316090 WIP - Persistent storage 2022-03-21 21:46:49 +01:00
Andras Bacsai
f5e7a84fa6 Update buildpacks for static sites 2022-03-21 21:25:01 +01:00
Andras Bacsai
c013764b61 WIP Persistent storage 2022-03-21 16:58:13 +01:00
Andras Bacsai
2320ab0dfc WIP - Persistent storage 2022-03-20 23:51:50 +01:00
Andras Bacsai
1281a0f7e4 Merge pull request #226 from coollabsio/v2.0.31
v2.0.31
2022-03-20 15:21:57 +01:00
Andras Bacsai
d8350cd4ee Migration file 2022-03-20 15:14:33 +01:00
Andras Bacsai
e3b7c23ed9 chore: Version++ 2022-03-20 15:05:05 +01:00
Andras Bacsai
eae1ea21d6 fix: Add nginx + htaccess files 2022-03-20 15:03:24 +01:00
Andras Bacsai
541aa76b64 fix: Only cleanup same app 2022-03-20 14:21:11 +01:00
Andras Bacsai
7b8555d524 fix: Cleanup old builds 2022-03-20 14:20:29 +01:00
Andras Bacsai
fdf998c181 css fix for select 2022-03-20 14:03:52 +01:00
Andras Bacsai
3d6b343adc remove mysql 2022-03-19 23:47:05 +01:00
Andras Bacsai
e338cecc14 feat: Add PHP modules 2022-03-19 23:46:33 +01:00
Andras Bacsai
e5537a33fb Merge pull request #223 from coollabsio/v2.0.30
v2.0.30
2022-03-19 15:11:42 +01:00
Andras Bacsai
35384deb68 fix: Remove build logs in case of app removed 2022-03-19 15:06:25 +01:00
Andras Bacsai
547ca60c2a fix: Better queue system + more support on monorepos 2022-03-19 15:04:52 +01:00
Andras Bacsai
376f6f7455 fix: Basedir for dockerfiles 2022-03-19 13:33:31 +01:00
Andras Bacsai
abe92dedff fix: no webhook secret found? 2022-03-15 17:35:37 +01:00
Andras Bacsai
4b521ceedc chore: version++ 2022-03-15 17:25:17 +01:00
Andras Bacsai
6dfcb9e52b fix: No error if GitSource is missing 2022-03-15 17:22:28 +01:00
Andras Bacsai
335e3216e2 fix: Missing session data 2022-03-15 17:21:18 +01:00
Andras Bacsai
5b22bb4818 fix: No cookie found 2022-03-15 17:04:15 +01:00
Andras Bacsai
0097004882 Merge pull request #217 from coollabsio/v2.0.29
v2.0.29
2022-03-12 00:28:26 +01:00
Andras Bacsai
1bc9e4c2d3 fix: Autodeploy true by default for GH repos 2022-03-11 23:56:11 +01:00
Andras Bacsai
36c7e1a3c3 feat: Install pnpm into docker image if pnpm lock file is used 2022-03-11 23:55:57 +01:00
Andras Bacsai
c6b4d04e26 Revert double build 2022-03-11 22:48:55 +01:00
Andras Bacsai
fa6cf068c7 feat: Autodeploy pause 2022-03-11 22:36:21 +01:00
Andras Bacsai
7c273a3a48 feat: Check ssl for new apps/services first 2022-03-11 21:28:27 +01:00
Andras Bacsai
3de2ea1523 chore: version++ 2022-03-11 21:19:03 +01:00
Andras Bacsai
c5c9f84503 feat: Webhooks inititate all applications with the correct branch 2022-03-11 21:18:12 +01:00
Andras Bacsai
16ea9a3e07 Update options request 2022-03-11 20:52:11 +01:00
Andras Bacsai
48f952c798 fix: Personal Gitlab repos 2022-03-11 20:47:26 +01:00
Andras Bacsai
f78ea5de07 Remove colors Tailwind 2022-03-11 20:47:13 +01:00
101 changed files with 2258 additions and 1037 deletions

View File

@@ -1,4 +1,5 @@
FROM node:16.14.0-alpine FROM node:16.14.0-alpine
RUN apk add --no-cache g++ cmake make python3
WORKDIR /app WORKDIR /app
COPY package*.json . COPY package*.json .
RUN yarn install RUN yarn install

View File

@@ -1,10 +1,10 @@
{ {
"name": "coolify", "name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.", "description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "2.0.28", "version": "2.2.0",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {
"dev": "docker-compose -f docker-compose-dev.yaml up -d && NODE_ENV=development svelte-kit dev --host 0.0.0.0", "dev": "docker-compose -f docker-compose-dev.yaml up -d && NODE_ENV=development svelte-kit dev",
"dev:stop": "docker-compose -f docker-compose-dev.yaml down", "dev:stop": "docker-compose -f docker-compose-dev.yaml down",
"dev:logs": "docker-compose -f docker-compose-dev.yaml logs -f --tail 10", "dev:logs": "docker-compose -f docker-compose-dev.yaml logs -f --tail 10",
"studio": "npx prisma studio", "studio": "npx prisma studio",
@@ -25,57 +25,59 @@
"prepare": "husky install" "prepare": "husky install"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-node": "1.0.0-next.70", "@sveltejs/adapter-node": "1.0.0-next.73",
"@sveltejs/adapter-static": "1.0.0-next.28", "@sveltejs/kit": "1.0.0-next.303",
"@sveltejs/kit": "1.0.0-next.288",
"@types/bcrypt": "5.0.0", "@types/bcrypt": "5.0.0",
"@types/js-cookie": "3.0.1", "@types/js-cookie": "3.0.1",
"@types/node": "17.0.21", "@types/js-yaml": "^4.0.5",
"@types/node-forge": "1.0.0", "@types/node": "17.0.23",
"@types/node-forge": "1.0.1",
"@typescript-eslint/eslint-plugin": "4.31.1", "@typescript-eslint/eslint-plugin": "4.31.1",
"@typescript-eslint/parser": "4.31.1", "@typescript-eslint/parser": "4.31.1",
"@zerodevx/svelte-toast": "0.7.0", "@zerodevx/svelte-toast": "0.7.1",
"autoprefixer": "10.4.2", "autoprefixer": "10.4.4",
"cross-var": "1.1.0", "cross-var": "1.1.0",
"eslint": "7.32.0", "eslint": "7.32.0",
"eslint-config-prettier": "8.4.0", "eslint-config-prettier": "8.5.0",
"eslint-plugin-svelte3": "3.4.1", "eslint-plugin-svelte3": "3.4.1",
"husky": "7.0.4", "husky": "7.0.4",
"lint-staged": "12.3.4", "lint-staged": "12.3.7",
"postcss": "8.4.7", "postcss": "8.4.12",
"prettier": "2.5.1", "prettier": "2.6.1",
"prettier-plugin-svelte": "2.6.0", "prettier-plugin-svelte": "2.6.0",
"prettier-plugin-tailwindcss": "0.1.8", "prettier-plugin-tailwindcss": "0.1.8",
"prisma": "3.10.0", "prisma": "3.11.1",
"svelte": "3.46.4", "svelte": "3.46.4",
"svelte-check": "2.4.5", "svelte-check": "2.4.6",
"svelte-preprocess": "4.10.4", "svelte-preprocess": "4.10.4",
"svelte-select": "^4.4.7",
"tailwindcss": "3.0.23", "tailwindcss": "3.0.23",
"ts-node": "10.6.0", "ts-node": "10.7.0",
"tslib": "2.3.1", "tslib": "2.3.1",
"typescript": "4.6.2" "typescript": "4.6.3"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@iarna/toml": "2.2.5", "@iarna/toml": "2.2.5",
"@prisma/client": "3.10.0", "@prisma/client": "3.11.1",
"@sentry/node": "6.18.1", "@sentry/node": "6.19.2",
"bcrypt": "5.0.1", "bcrypt": "5.0.1",
"bullmq": "1.76.0", "bullmq": "1.78.1",
"compare-versions": "4.1.3", "compare-versions": "4.1.3",
"cookie": "0.4.2", "cookie": "0.4.2",
"cooltipz-css": "^2.1.0",
"cuid": "2.1.8", "cuid": "2.1.8",
"dayjs": "1.10.8", "dayjs": "1.11.0",
"dockerode": "3.3.1", "dockerode": "3.3.1",
"dotenv-extended": "2.9.0", "dotenv-extended": "2.9.0",
"generate-password": "1.7.0", "generate-password": "1.7.0",
"get-port": "6.1.2", "get-port": "6.1.2",
"got": "12.0.1", "got": "12.0.2",
"js-cookie": "3.0.1", "js-cookie": "3.0.1",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsonwebtoken": "8.5.1", "jsonwebtoken": "8.5.1",
"mustache": "^4.2.0", "mustache": "^4.2.0",
"node-forge": "1.2.1", "node-forge": "1.3.0",
"svelte-kit-cookie-session": "2.1.2", "svelte-kit-cookie-session": "2.1.2",
"tailwindcss-scrollbar": "^0.1.0", "tailwindcss-scrollbar": "^0.1.0",
"unique-names-generator": "4.7.1" "unique-names-generator": "4.7.1"

537
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Application" ADD COLUMN "phpModules" TEXT;

View File

@@ -0,0 +1,18 @@
-- CreateTable
CREATE TABLE "ApplicationPersistentStorage" (
"id" TEXT NOT NULL PRIMARY KEY,
"applicationId" TEXT NOT NULL,
"path" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "ApplicationPersistentStorage_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "ApplicationPersistentStorage_applicationId_key" ON "ApplicationPersistentStorage"("applicationId");
-- CreateIndex
CREATE UNIQUE INDEX "ApplicationPersistentStorage_path_key" ON "ApplicationPersistentStorage"("path");
-- CreateIndex
CREATE UNIQUE INDEX "ApplicationPersistentStorage_applicationId_path_key" ON "ApplicationPersistentStorage"("applicationId", "path");

View File

@@ -0,0 +1,19 @@
-- CreateTable
CREATE TABLE "Ghost" (
"id" TEXT NOT NULL PRIMARY KEY,
"defaultEmail" TEXT NOT NULL,
"defaultPassword" TEXT NOT NULL,
"mariadbUser" TEXT NOT NULL,
"mariadbPassword" TEXT NOT NULL,
"mariadbRootUser" TEXT NOT NULL,
"mariadbRootUserPassword" TEXT NOT NULL,
"mariadbDatabase" TEXT,
"mariadbPublicPort" INTEGER,
"serviceId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Ghost_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "Ghost_serviceId_key" ON "Ghost"("serviceId");

View File

@@ -72,9 +72,9 @@ model TeamInvitation {
} }
model Application { model Application {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
fqdn String? @unique fqdn String? @unique
repository String? repository String?
configHash String? configHash String?
branch String? branch String?
@@ -86,15 +86,17 @@ model Application {
startCommand String? startCommand String?
baseDirectory String? baseDirectory String?
publishDirectory String? publishDirectory String?
createdAt DateTime @default(now()) phpModules String?
updatedAt DateTime @updatedAt createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
settings ApplicationSettings? settings ApplicationSettings?
teams Team[] teams Team[]
destinationDockerId String? destinationDockerId String?
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id]) destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
gitSourceId String? gitSourceId String?
gitSource GitSource? @relation(fields: [gitSourceId], references: [id]) gitSource GitSource? @relation(fields: [gitSourceId], references: [id])
secrets Secret[] secrets Secret[]
persistentStorage ApplicationPersistentStorage[]
} }
model ApplicationSettings { model ApplicationSettings {
@@ -104,10 +106,22 @@ model ApplicationSettings {
dualCerts Boolean @default(false) dualCerts Boolean @default(false)
debug Boolean @default(false) debug Boolean @default(false)
previews Boolean @default(false) previews Boolean @default(false)
autodeploy Boolean @default(true)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model ApplicationPersistentStorage {
id String @id @default(cuid())
application Application @relation(fields: [applicationId], references: [id])
applicationId String @unique
path String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([applicationId, path])
}
model Secret { model Secret {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
@@ -264,6 +278,7 @@ model Service {
minio Minio? minio Minio?
vscodeserver Vscodeserver? vscodeserver Vscodeserver?
wordpress Wordpress? wordpress Wordpress?
ghost Ghost?
serviceSecret ServiceSecret[] serviceSecret ServiceSecret[]
} }
@@ -318,3 +333,19 @@ model Wordpress {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model Ghost {
id String @id @default(cuid())
defaultEmail String
defaultPassword String
mariadbUser String
mariadbPassword String
mariadbRootUser String
mariadbRootUserPassword String
mariadbDatabase String?
mariadbPublicPort Int?
serviceId String @unique
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

8
src/app.d.ts vendored
View File

@@ -7,7 +7,13 @@ declare namespace App {
} }
interface Platform {} interface Platform {}
interface Session extends SessionData {} interface Session extends SessionData {}
interface Stuff {} interface Stuff {
service: any;
application: any;
isRunning: boolean;
appId: string;
readOnly: boolean;
}
} }
interface SessionData { interface SessionData {

View File

@@ -29,10 +29,10 @@ export function makeLabelForStandaloneApplication({
fqdn = `${protocol}://${pullmergeRequestId}.${domain}`; fqdn = `${protocol}://${pullmergeRequestId}.${domain}`;
} }
return [ return [
'--label coolify.managed=true', 'coolify.managed=true',
`--label coolify.version=${version}`, `coolify.version=${version}`,
`--label coolify.type=standalone-application`, `coolify.type=standalone-application`,
`--label coolify.configuration=${base64Encode( `coolify.configuration=${base64Encode(
JSON.stringify({ JSON.stringify({
applicationId, applicationId,
fqdn, fqdn,
@@ -84,7 +84,15 @@ export function makeLabelForServices(type) {
} }
export const setDefaultConfiguration = async (data) => { export const setDefaultConfiguration = async (data) => {
let { buildPack, port, installCommand, startCommand, buildCommand, publishDirectory } = data; let {
buildPack,
port,
installCommand,
startCommand,
buildCommand,
publishDirectory,
baseDirectory
} = data;
const template = scanningTemplates[buildPack]; const template = scanningTemplates[buildPack];
if (!port) { if (!port) {
port = template?.port || 3000; port = template?.port || 3000;
@@ -97,6 +105,10 @@ export const setDefaultConfiguration = async (data) => {
if (!startCommand) startCommand = template?.startCommand || 'yarn start'; if (!startCommand) startCommand = template?.startCommand || 'yarn start';
if (!buildCommand) buildCommand = template?.buildCommand || null; if (!buildCommand) buildCommand = template?.buildCommand || null;
if (!publishDirectory) publishDirectory = template?.publishDirectory || null; if (!publishDirectory) publishDirectory = template?.publishDirectory || null;
if (baseDirectory) {
if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`;
if (!baseDirectory.endsWith('/')) baseDirectory = `${baseDirectory}/`;
}
return { return {
buildPack, buildPack,
@@ -104,7 +116,8 @@ export const setDefaultConfiguration = async (data) => {
installCommand, installCommand,
startCommand, startCommand,
buildCommand, buildCommand,
publishDirectory publishDirectory,
baseDirectory
}; };
}; };
@@ -122,6 +135,7 @@ export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, ap
RewriteRule ^(.+)$ index.php [QSA,L] RewriteRule ^(.+)$ index.php [QSA,L]
` `
); );
await fs.writeFile(`${workdir}/entrypoint.sh`, `chown -R 1000 /app`);
saveBuildLog({ line: 'Copied default configuration file for PHP.', buildId, applicationId }); saveBuildLog({ line: 'Copied default configuration file for PHP.', buildId, applicationId });
} else if (staticDeployments.includes(buildPack)) { } else if (staticDeployments.includes(buildPack)) {
await fs.writeFile( await fs.writeFile(
@@ -129,27 +143,35 @@ export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, ap
`user nginx; `user nginx;
worker_processes auto; worker_processes auto;
error_log /var/log/nginx/error.log warn; error_log /docker.stdout;
pid /var/run/nginx.pid; pid /run/nginx.pid;
events { events {
worker_connections 1024; worker_connections 1024;
} }
http { http {
include /etc/nginx/mime.types; log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
access_log off; '"$http_user_agent" "$http_x_forwarded_for"';
sendfile on;
#tcp_nopush on; access_log /docker.stdout main;
keepalive_timeout 65;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
server { server {
listen 80; listen 80;
server_name localhost; server_name localhost;
location / { location / {
root /usr/share/nginx/html; root /app;
index index.html; index index.html;
try_files $uri $uri/index.html $uri/ /index.html =404; try_files $uri $uri/index.html $uri/ /index.html =404;
} }
@@ -160,7 +182,7 @@ export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, ap
# #
error_page 500 502 503 504 /50x.html; error_page 500 502 503 504 /50x.html;
location = /50x.html { location = /50x.html {
root /usr/share/nginx/html; root /app;
} }
} }
@@ -175,3 +197,11 @@ export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, ap
throw new Error(error); throw new Error(error);
} }
} }
export function checkPnpm(installCommand = null, buildCommand = null, startCommand = null) {
return (
installCommand?.includes('pnpm') ||
buildCommand?.includes('pnpm') ||
startCommand?.includes('pnpm')
);
}

View File

@@ -16,6 +16,7 @@ export default async function ({
let file = `${workdir}/Dockerfile`; let file = `${workdir}/Dockerfile`;
if (baseDirectory) { if (baseDirectory) {
file = `${workdir}/${baseDirectory}/Dockerfile`; file = `${workdir}/${baseDirectory}/Dockerfile`;
workdir = `${workdir}/${baseDirectory}`;
} }
const Dockerfile: Array<string> = (await fs.readFile(`${file}`, 'utf8')) const Dockerfile: Array<string> = (await fs.readFile(`${file}`, 'utf8'))

View File

@@ -6,17 +6,17 @@ const createDockerfile = async (data, imageforBuild): Promise<void> => {
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageforBuild}`); Dockerfile.push(`FROM ${imageforBuild}`);
Dockerfile.push('WORKDIR /usr/share/nginx/html'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`); Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/src/app/${publishDirectory} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE 80`);
Dockerfile.push('CMD ["nginx", "-g", "daemon off;"]');
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };
export default async function (data) { export default async function (data) {
try { try {
const image = 'nginx:stable-alpine'; const image = 'webdevops/nginx:alpine';
const imageForBuild = 'node:lts'; const imageForBuild = 'node:lts';
await buildCacheImageWithNode(data, imageForBuild); await buildCacheImageWithNode(data, imageForBuild);

View File

@@ -4,13 +4,17 @@ import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
const { applicationId, tag, port, startCommand, workdir, baseDirectory } = data; const { applicationId, tag, port, startCommand, workdir, baseDirectory } = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
const isPnpm = startCommand.includes('pnpm');
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /usr/src/app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`); Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push( if (isPnpm) {
`COPY --from=${applicationId}:${tag}-cache /usr/src/app/${baseDirectory || ''} ./` 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 --from=${applicationId}:${tag}-cache /app/${baseDirectory || ''} ./`);
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push(`CMD ${startCommand}`); Dockerfile.push(`CMD ${startCommand}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));

View File

@@ -1,5 +1,6 @@
import { buildImage } from '$lib/docker'; import { buildImage } from '$lib/docker';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { checkPnpm } from './common';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
const { const {
@@ -13,9 +14,9 @@ const createDockerfile = async (data, image): Promise<void> => {
pullmergeRequestId pullmergeRequestId
} = data; } = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
const isPnpm = checkPnpm(installCommand, buildCommand, startCommand);
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /usr/src/app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`); Dockerfile.push(`LABEL coolify.image=true`);
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
@@ -32,17 +33,13 @@ const createDockerfile = async (data, image): Promise<void> => {
} }
}); });
} }
Dockerfile.push(`COPY ./${baseDirectory || ''}package*.json ./`); if (isPnpm) {
try { Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm');
await fs.stat(`${workdir}/yarn.lock`); Dockerfile.push('RUN pnpm add -g pnpm');
Dockerfile.push(`COPY ./${baseDirectory || ''}yarn.lock ./`); }
} catch (error) {} Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
try {
await fs.stat(`${workdir}/pnpm-lock.yaml`);
Dockerfile.push(`COPY ./${baseDirectory || ''}pnpm-lock.yaml ./`);
} catch (error) {}
Dockerfile.push(`RUN ${installCommand}`); Dockerfile.push(`RUN ${installCommand}`);
Dockerfile.push(`COPY ./${baseDirectory || ''} ./`);
if (buildCommand) { if (buildCommand) {
Dockerfile.push(`RUN ${buildCommand}`); Dockerfile.push(`RUN ${buildCommand}`);
} }

View File

@@ -1,5 +1,6 @@
import { buildImage } from '$lib/docker'; import { buildImage } from '$lib/docker';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { checkPnpm } from './common';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
const { const {
@@ -13,9 +14,10 @@ const createDockerfile = async (data, image): Promise<void> => {
pullmergeRequestId pullmergeRequestId
} = data; } = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
const isPnpm = checkPnpm(installCommand, buildCommand, startCommand);
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /usr/src/app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`); Dockerfile.push(`LABEL coolify.image=true`);
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
@@ -32,17 +34,12 @@ const createDockerfile = async (data, image): Promise<void> => {
} }
}); });
} }
Dockerfile.push(`COPY ./${baseDirectory || ''}package*.json ./`); if (isPnpm) {
try { Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm');
await fs.stat(`${workdir}/yarn.lock`); Dockerfile.push('RUN pnpm add -g pnpm');
Dockerfile.push(`COPY ./${baseDirectory || ''}yarn.lock ./`); }
} catch (error) {} Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
try {
await fs.stat(`${workdir}/pnpm-lock.yaml`);
Dockerfile.push(`COPY ./${baseDirectory || ''}pnpm-lock.yaml ./`);
} catch (error) {}
Dockerfile.push(`RUN ${installCommand}`); Dockerfile.push(`RUN ${installCommand}`);
Dockerfile.push(`COPY ./${baseDirectory || ''} ./`);
if (buildCommand) { if (buildCommand) {
Dockerfile.push(`RUN ${buildCommand}`); Dockerfile.push(`RUN ${buildCommand}`);
} }

View File

@@ -1,5 +1,6 @@
import { buildImage } from '$lib/docker'; import { buildImage } from '$lib/docker';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { checkPnpm } from './common';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
const { const {
@@ -13,9 +14,9 @@ const createDockerfile = async (data, image): Promise<void> => {
pullmergeRequestId pullmergeRequestId
} = data; } = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
const isPnpm = checkPnpm(installCommand, buildCommand, startCommand);
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /usr/src/app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`); Dockerfile.push(`LABEL coolify.image=true`);
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
@@ -32,17 +33,12 @@ const createDockerfile = async (data, image): Promise<void> => {
} }
}); });
} }
Dockerfile.push(`COPY ./${baseDirectory || ''}package*.json ./`); if (isPnpm) {
try { Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm');
await fs.stat(`${workdir}/yarn.lock`); Dockerfile.push('RUN pnpm add -g pnpm');
Dockerfile.push(`COPY ./${baseDirectory || ''}yarn.lock ./`); }
} catch (error) {} Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
try {
await fs.stat(`${workdir}/pnpm-lock.yaml`);
Dockerfile.push(`COPY ./${baseDirectory || ''}pnpm-lock.yaml ./`);
} catch (error) {}
Dockerfile.push(`RUN ${installCommand}`); Dockerfile.push(`RUN ${installCommand}`);
Dockerfile.push(`COPY ./${baseDirectory || ''} ./`);
if (buildCommand) { if (buildCommand) {
Dockerfile.push(`RUN ${buildCommand}`); Dockerfile.push(`RUN ${buildCommand}`);
} }

View File

@@ -4,21 +4,19 @@ import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
const { workdir, baseDirectory } = data; const { workdir, baseDirectory } = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push(`LABEL coolify.image=true`); Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push('RUN a2enmod rewrite'); Dockerfile.push('WORKDIR /app');
Dockerfile.push('WORKDIR /var/www/html'); Dockerfile.push(`COPY .${baseDirectory || ''} /app`);
Dockerfile.push(`COPY ./${baseDirectory || ''} /var/www/html`); Dockerfile.push(`COPY /.htaccess .`);
Dockerfile.push(`COPY /entrypoint.sh /opt/docker/provision/entrypoint.d/30-entrypoint.sh`);
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE 80`);
Dockerfile.push('CMD ["apache2-foreground"]');
Dockerfile.push('RUN chown -R www-data /var/www/html');
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };
export default async function (data) { export default async function (data) {
try { try {
const image = 'php:apache'; const image = 'webdevops/php-nginx';
await createDockerfile(data, image); await createDockerfile(data, image);
await buildImage(data); await buildImage(data);
} catch (error) { } catch (error) {

View File

@@ -7,16 +7,16 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push(`LABEL coolify.image=true`); Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push('WORKDIR /usr/share/nginx/html'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/src/app/${publishDirectory} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE 80`);
Dockerfile.push('CMD ["nginx", "-g", "daemon off;"]');
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };
export default async function (data) { export default async function (data) {
try { try {
const image = 'nginx:stable-alpine'; const image = 'webdevops/nginx:alpine';
const imageForBuild = 'node:lts'; const imageForBuild = 'node:lts';
await buildCacheImageWithNode(data, imageForBuild); await buildCacheImageWithNode(data, imageForBuild);
await createDockerfile(data, image); await createDockerfile(data, image);

View File

@@ -7,23 +7,21 @@ const createDockerfile = async (data, image, name): Promise<void> => {
const { workdir, port, applicationId, tag } = data; const { workdir, port, applicationId, tag } = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /usr/src/app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`); Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/src/app/target target`); 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 --from=${applicationId}:${tag}-cache /usr/local/cargo /usr/local/cargo`);
Dockerfile.push(`COPY . .`); Dockerfile.push(`COPY . .`);
Dockerfile.push(`RUN cargo build --release --bin ${name}`); Dockerfile.push(`RUN cargo build --release --bin ${name}`);
Dockerfile.push('FROM debian:buster-slim'); Dockerfile.push('FROM debian:buster-slim');
Dockerfile.push('WORKDIR /usr/src/app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push( Dockerfile.push(
`RUN apt-get update -y && apt-get install -y --no-install-recommends openssl libcurl4 ca-certificates && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/*` `RUN apt-get update -y && apt-get install -y --no-install-recommends openssl libcurl4 ca-certificates && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/*`
); );
Dockerfile.push(`RUN update-ca-certificates`); Dockerfile.push(`RUN update-ca-certificates`);
Dockerfile.push( Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/target/release/${name} ${name}`);
`COPY --from=${applicationId}:${tag}-cache /usr/src/app/target/release/${name} ${name}`
);
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push(`CMD ["/usr/src/app/${name}"]`); Dockerfile.push(`CMD ["/app/${name}"]`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };

View File

@@ -15,7 +15,7 @@ const createDockerfile = async (data, image): Promise<void> => {
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /usr/share/nginx/html'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`); Dockerfile.push(`LABEL coolify.image=true`);
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
@@ -33,20 +33,18 @@ const createDockerfile = async (data, image): Promise<void> => {
}); });
} }
if (buildCommand) { if (buildCommand) {
Dockerfile.push( Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
`COPY --from=${applicationId}:${tag}-cache /usr/src/app/${publishDirectory} ./`
);
} else { } else {
Dockerfile.push(`COPY ./${baseDirectory || ''} ./`); Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
} }
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE 80`);
Dockerfile.push('CMD ["nginx", "-g", "daemon off;"]');
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };
export default async function (data) { export default async function (data) {
try { try {
const image = 'nginx:stable-alpine'; const image = 'webdevops/nginx:alpine';
const imageForBuild = 'node:lts'; const imageForBuild = 'node:lts';
if (data.buildCommand) await buildCacheImageWithNode(data, imageForBuild); if (data.buildCommand) await buildCacheImageWithNode(data, imageForBuild);
await createDockerfile(data, image); await createDockerfile(data, image);

View File

@@ -6,17 +6,17 @@ const createDockerfile = async (data, image): Promise<void> => {
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /usr/share/nginx/html'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`); Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/src/app/${publishDirectory} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE 80`);
Dockerfile.push('CMD ["nginx", "-g", "daemon off;"]');
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };
export default async function (data) { export default async function (data) {
try { try {
const image = 'nginx:stable-alpine'; const image = 'webdevops/nginx:alpine';
const imageForBuild = 'node:lts'; const imageForBuild = 'node:lts';
await buildCacheImageWithNode(data, imageForBuild); await buildCacheImageWithNode(data, imageForBuild);

View File

@@ -6,17 +6,17 @@ const createDockerfile = async (data, image): Promise<void> => {
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /usr/share/nginx/html'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`); Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/src/app/${publishDirectory} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE 80`);
Dockerfile.push('CMD ["nginx", "-g", "daemon off;"]');
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };
export default async function (data) { export default async function (data) {
try { try {
const image = 'nginx:stable-alpine'; const image = 'webdevops/nginx:alpine';
const imageForBuild = 'node:lts'; const imageForBuild = 'node:lts';
await buildCacheImageWithNode(data, imageForBuild); await buildCacheImageWithNode(data, imageForBuild);
await createDockerfile(data, image); await createDockerfile(data, image);

View File

@@ -11,6 +11,7 @@ import { version as currentVersion } from '../../package.json';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import Cookie from 'cookie'; import Cookie from 'cookie';
import os from 'os'; import os from 'os';
import cuid from 'cuid';
try { try {
if (!dev) { if (!dev) {
@@ -68,9 +69,9 @@ export const isTeamIdTokenAvailable = (request) => {
export const getTeam = (event) => { export const getTeam = (event) => {
const cookies = Cookie.parse(event.request.headers.get('cookie')); const cookies = Cookie.parse(event.request.headers.get('cookie'));
if (cookies.teamId) { if (cookies?.teamId) {
return cookies.teamId; return cookies.teamId;
} else if (event.locals.session.data.teamId) { } else if (event.locals.session.data?.teamId) {
return event.locals.session.data.teamId; return event.locals.session.data.teamId;
} }
return null; return null;

View File

@@ -9,7 +9,16 @@ export const dateOptions: DateTimeFormatOptions = {
hour12: false hour12: false
}; };
export const staticDeployments = ['react', 'vuejs', 'static', 'svelte', 'gatsby', 'php']; export const staticDeployments = [
'react',
'vuejs',
'static',
'svelte',
'gatsby',
'php',
'astro',
'eleventy'
];
export const notNodeDeployments = ['php', 'docker', 'rust']; export const notNodeDeployments = ['php', 'docker', 'rust'];
export function getDomain(domain) { export function getDomain(domain) {

View File

@@ -0,0 +1,9 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<img
alt="ghost logo"
class={isAbsolute ? 'w-12 absolute top-0 left-0 -m-3 -mt-5' : 'w-8 mx-auto'}
src="/ghost.png"
/>

View File

@@ -0,0 +1,24 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<svg
class={isAbsolute ? 'w-12 h-12 absolute top-0 left-0 -m-5' : 'w-8 mx-auto'}
viewBox="0 0 220 105"
>
<g>
<path
fill="#FF6D5A"
d="M183.9,0.2c-9.8,0-18,6.7-20.3,15.8h-29.2c-11.5,0-20.8,9.3-20.8,20.8c0,5.7-4.7,10.4-10.4,10.4H99
c-2.3-9.1-10.5-15.8-20.3-15.8c-9.8,0-18,6.7-20.3,15.8H41.7c-2.3-9.1-10.5-15.8-20.3-15.8c-11.6,0-21,9.4-21,21
c0,11.6,9.4,21,21,21c9.8,0,18-6.7,20.3-15.8h16.7c2.3,9.1,10.5,15.8,20.3,15.8c9.7,0,17.9-6.6,20.3-15.6h4.2
c5.7,0,10.4,4.7,10.4,10.4c0,11.5,9.3,20.8,20.8,20.8h6.8c2.3,9.1,10.5,15.8,20.3,15.8c11.6,0,21-9.4,21-21c0-11.6-9.4-21-21-21
c-9.8,0-18,6.7-20.3,15.8h-6.8c-5.7,0-10.4-4.7-10.4-10.4c0-6.3-2.8-11.9-7.2-15.7c4.4-3.8,7.2-9.4,7.2-15.7
c0-5.7,4.7-10.4,10.4-10.4h29.2c2.3,9.1,10.5,15.8,20.3,15.8c11.6,0,21-9.4,21-21C204.9,9.6,195.5,0.2,183.9,0.2z M21.4,63
c-5.8,0-10.6-4.8-10.6-10.6s4.8-10.6,10.6-10.6S32,46.6,32,52.4S27.3,63,21.4,63z M78.7,63c-5.8,0-10.6-4.8-10.6-10.6
s4.8-10.6,10.6-10.6s10.6,4.8,10.6,10.6S84.6,63,78.7,63z M161.5,73.2c5.8,0,10.6,4.8,10.6,10.6s-4.8,10.6-10.6,10.6
s-10.6-4.8-10.6-10.6C150.9,77.9,155.7,73.2,161.5,73.2z M183.9,31.8c-5.8,0-10.6-4.8-10.6-10.6s4.8-10.6,10.6-10.6
s10.6,4.8,10.6,10.6C194.5,27,189.8,31.8,183.9,31.8z"
/>
</g>
</svg>

View File

@@ -0,0 +1,159 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<svg
class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-10 h-10 mx-auto'}
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 640 640"
width="640"
height="640"
><defs
><path
d="M407.55 916.24C471.25 916.24 522.89 967.88 522.89 1031.57C522.89 1113.88 522.89 1245.44 522.89 1327.74C522.89 1391.44 471.25 1443.08 407.55 1443.08C325.25 1443.08 193.68 1443.08 111.38 1443.08C47.69 1443.08 -3.95 1391.44 -3.95 1327.74C-3.95 1245.44 -3.95 1113.88 -3.95 1031.57C-3.95 967.88 47.69 916.24 111.38 916.24C193.68 916.24 325.25 916.24 407.55 916.24Z"
id="a1LdTs1gvU"
/><linearGradient
id="gradientcoH7TNh19"
gradientUnits="userSpaceOnUse"
x1="256.07"
y1="1132.14"
x2="609.11"
y2="1480.42"
><stop style="stop-color: #c2efd2;stop-opacity: 1" offset="0%" /><stop
style="stop-color: #8ff0e5;stop-opacity: 1"
offset="100%"
/></linearGradient
><path
d="M-467.41 394.63C-467.41 554.76 -597.42 684.76 -757.55 684.76C-917.68 684.76 -1047.69 554.76 -1047.69 394.63C-1047.69 234.5 -917.68 104.49 -757.55 104.49C-597.42 104.49 -467.41 234.5 -467.41 394.63Z"
id="a1uaEBd4xM"
/><path
d="M-96.99 -586.14C-57.24 -619.85 -5.79 -604.75 19.26 -580.46C31.43 -568.66 56.57 -546.36 40.97 -491.67C32.76 -462.87 10.41 -436.4 -26.05 -412.27C-15.07 -377.85 -5.6 -344.76 2.36 -313C14.29 -265.36 13.55 -189.67 -26.05 -155.4C-67.27 -119.73 -166.91 -104.09 -234.24 -103.09C-301.57 -102.1 -406.19 -113.09 -461.6 -155.4C-517.01 -197.7 -512.24 -257.07 -498.04 -313C-488.58 -350.28 -476.43 -383.38 -461.6 -412.27C-505.54 -441.3 -530.54 -467.76 -536.6 -491.67C-545.68 -527.54 -530.93 -565.61 -501.12 -586.14C-471.31 -606.67 -435.18 -606.9 -400.45 -586.14C-377.3 -572.3 -354.79 -542.13 -332.92 -495.62C-287.85 -505.25 -254.96 -509.57 -234.24 -508.6C-214.74 -507.68 -186.57 -503.36 -149.72 -495.62C-135.81 -537.95 -118.23 -568.12 -96.99 -586.14Z"
id="f8p7QlEjN3"
/><linearGradient
id="gradienta4Tg99ZOOp"
gradientUnits="userSpaceOnUse"
x1="-440.25"
y1="-388.59"
x2="-100.49"
y2="-147.33"
><stop style="stop-color: #5cdd8b;stop-opacity: 1" offset="0%" /><stop
style="stop-color: #7ae6a1;stop-opacity: 1"
offset="100%"
/></linearGradient
><path
d="M-86.03 -10.69C-61.35 -10.69 -41.34 9.32 -41.34 34.01C-41.34 119.07 -41.34 329.58 -41.34 414.65C-41.34 439.33 -61.35 459.34 -86.03 459.34C-136.01 459.34 -241.25 459.34 -291.23 459.34C-315.92 459.34 -335.93 439.33 -335.93 414.65C-335.93 329.58 -335.93 119.07 -335.93 34.01C-335.93 9.32 -315.92 -10.69 -291.23 -10.69C-241.25 -10.69 -136.01 -10.69 -86.03 -10.69Z"
id="d32ZZRxd1S"
/><linearGradient
id="gradientb1JxIe4xUm"
gradientUnits="userSpaceOnUse"
x1="-791.65"
y1="-33.27"
x2="892.1"
y2="418.94"
><stop style="stop-color: #5cdd8b;stop-opacity: 1" offset="0%" /><stop
style="stop-color: #5ae98f;stop-opacity: 1"
offset="100%"
/></linearGradient
><path
d="M-257.95 458.12C-247.92 449.62 -234.93 453.43 -228.61 459.56C-225.54 462.54 -219.19 468.17 -223.13 481.97C-225.2 489.24 -230.84 495.92 -240.05 502.01C-237.27 510.7 -234.88 519.06 -232.88 527.07C-229.86 539.1 -230.05 558.21 -240.05 566.86C-250.45 575.86 -275.6 579.81 -292.6 580.06C-309.6 580.31 -336.01 577.54 -349.99 566.86C-363.98 556.18 -362.77 541.19 -359.19 527.07C-356.8 517.66 -353.73 509.31 -349.99 502.01C-361.08 494.69 -367.39 488.01 -368.92 481.97C-371.22 472.92 -367.49 463.31 -359.97 458.12C-352.44 452.94 -343.32 452.88 -334.56 458.12C-328.71 461.62 -323.03 469.23 -317.51 480.97C-306.13 478.54 -297.83 477.45 -292.6 477.7C-287.68 477.93 -280.56 479.02 -271.26 480.97C-267.75 470.29 -263.32 462.67 -257.95 458.12Z"
id="b19LRRbPrG"
/><path
d="M490.4 235.64C544.09 358.38 544.09 435.34 490.4 466.5C409.85 513.24 199.96 527.49 139.54 455.64C99.26 407.74 99.26 334.4 139.54 235.64C180.5 168.18 238.71 134.45 314.17 134.45C389.64 134.45 448.38 168.18 490.4 235.64Z"
id="bN5StdyPU"
/><linearGradient
id="gradientb1HT15TsY0"
gradientUnits="userSpaceOnUse"
x1="259.78"
y1="261.15"
x2="463.85"
y2="456.49"
><stop style="stop-color: #5cdd8b;stop-opacity: 1" offset="0%" /><stop
style="stop-color: #86e6a9;stop-opacity: 1"
offset="100%"
/></linearGradient
><path
d="M393.81 -775.89C428.26 -748.09 439.99 -725.54 429 -708.22C412.51 -682.24 353.16 -646.07 324.5 -657.93C305.39 -665.83 294.22 -687.32 290.97 -722.41C292.69 -748.43 304.61 -767.19 326.73 -778.69C348.85 -790.19 371.21 -789.26 393.81 -775.89Z"
id="arh6miPP2"
/><linearGradient
id="gradientc2g6rBSAiq"
gradientUnits="userSpaceOnUse"
x1="330.1"
y1="-733.26"
x2="419.69"
y2="-707.1"
><stop style="stop-color: #5cdd8b;stop-opacity: 1" offset="0%" /><stop
style="stop-color: #86e6a9;stop-opacity: 1"
offset="100%"
/></linearGradient
><path
d="M675.36 -369.24C669.97 -325.31 657.02 -303.43 636.51 -303.61C605.74 -303.87 543.67 -335.15 538.59 -365.74C535.2 -386.14 547.54 -406.99 575.61 -428.29C598.61 -440.58 620.83 -440.37 642.29 -427.67C663.74 -414.97 674.77 -395.49 675.36 -369.24Z"
id="a2VENFzCvL"
/><linearGradient
id="gradientc18GuJy4sZ"
gradientUnits="userSpaceOnUse"
x1="605.5"
y1="-400.8"
x2="630.64"
y2="-310.92"
><stop style="stop-color: #5cdd8b;stop-opacity: 1" offset="0%" /><stop
style="stop-color: #86e6a9;stop-opacity: 1"
offset="100%"
/></linearGradient
></defs
><g
><g
><g><use xlink:href="#a1LdTs1gvU" opacity="1" fill="url(#gradientcoH7TNh19)" /></g><g
><use xlink:href="#a1uaEBd4xM" opacity="1" fill="#ebf0ed" fill-opacity="1" /></g
><g
><use xlink:href="#f8p7QlEjN3" opacity="1" fill="url(#gradienta4Tg99ZOOp)" /><g
><use
xlink:href="#f8p7QlEjN3"
opacity="1"
fill-opacity="0"
stroke="#ffffff"
stroke-width="98"
stroke-opacity="0.57"
/></g
></g
><g
><use xlink:href="#d32ZZRxd1S" opacity="1" fill="url(#gradientb1JxIe4xUm)" /><g
><use
xlink:href="#d32ZZRxd1S"
opacity="1"
fill-opacity="0"
stroke="#f2f2f2"
stroke-width="60"
stroke-opacity="0.51"
/></g
></g
><g
><use xlink:href="#b19LRRbPrG" opacity="1" fill="#d8ad9a" fill-opacity="1" /><g
><use
xlink:href="#b19LRRbPrG"
opacity="1"
fill-opacity="0"
stroke="#ffffff"
stroke-width="17"
stroke-opacity="1"
/></g
></g
><g
><use xlink:href="#bN5StdyPU" opacity="1" fill="url(#gradientb1HT15TsY0)" /><g
><use
xlink:href="#bN5StdyPU"
opacity="1"
fill-opacity="0"
stroke="#f2f2f2"
stroke-width="200"
stroke-opacity="0.51"
/></g
></g
><g><use xlink:href="#arh6miPP2" opacity="1" fill="url(#gradientc2g6rBSAiq)" /></g><g
><use xlink:href="#a2VENFzCvL" opacity="1" fill="url(#gradientc18GuJy4sZ)" /></g
></g
></g
></svg
>

View File

@@ -13,7 +13,7 @@ export function findBuildPack(pack, packageManager = 'npm') {
if (pack === 'node') { if (pack === 'node') {
return { return {
...metaData, ...metaData,
installCommand: null, ...defaultBuildAndDeploy(packageManager),
buildCommand: null, buildCommand: null,
startCommand: null, startCommand: null,
publishDirectory: null, publishDirectory: null,

View File

@@ -58,29 +58,22 @@ export async function removeApplication({ id, teamId }) {
const id = containerObj.ID; const id = containerObj.ID;
const preview = containerObj.Image.split('-')[1]; const preview = containerObj.Image.split('-')[1];
await removeDestinationDocker({ id, engine: destinationDocker.engine }); await removeDestinationDocker({ id, engine: destinationDocker.engine });
try {
if (preview) {
await removeProxyConfiguration({ domain: `${preview}.${domain}` });
} else {
await removeProxyConfiguration({ domain });
}
} catch (error) {
console.log(error);
}
} }
} }
} }
await prisma.applicationSettings.deleteMany({ where: { application: { id } } }); await prisma.applicationSettings.deleteMany({ where: { application: { id } } });
await prisma.buildLog.deleteMany({ where: { applicationId: id } }); await prisma.buildLog.deleteMany({ where: { applicationId: id } });
await prisma.build.deleteMany({ where: { applicationId: id } });
await prisma.secret.deleteMany({ where: { applicationId: id } }); await prisma.secret.deleteMany({ where: { applicationId: id } });
await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: id } });
await prisma.application.deleteMany({ where: { id, teams: { some: { id: teamId } } } }); await prisma.application.deleteMany({ where: { id, teams: { some: { id: teamId } } } });
} }
export async function getApplicationWebhook({ projectId, branch }) { export async function getApplicationWebhook({ projectId, branch }) {
try { try {
let body = await prisma.application.findFirst({ let application = await prisma.application.findFirst({
where: { projectId, branch }, where: { projectId, branch, settings: { autodeploy: true } },
include: { include: {
destinationDocker: true, destinationDocker: true,
settings: true, settings: true,
@@ -88,30 +81,41 @@ export async function getApplicationWebhook({ projectId, branch }) {
secrets: true secrets: true
} }
}); });
if (!application) {
if (body.gitSource?.githubApp?.clientSecret) { return null;
body.gitSource.githubApp.clientSecret = decrypt(body.gitSource.githubApp.clientSecret);
} }
if (body.gitSource?.githubApp?.webhookSecret) { if (application?.gitSource?.githubApp?.clientSecret) {
body.gitSource.githubApp.webhookSecret = decrypt(body.gitSource.githubApp.webhookSecret); application.gitSource.githubApp.clientSecret = decrypt(
application.gitSource.githubApp.clientSecret
);
} }
if (body.gitSource?.githubApp?.privateKey) { if (application?.gitSource?.githubApp?.webhookSecret) {
body.gitSource.githubApp.privateKey = decrypt(body.gitSource.githubApp.privateKey); application.gitSource.githubApp.webhookSecret = decrypt(
application.gitSource.githubApp.webhookSecret
);
} }
if (body?.gitSource?.gitlabApp?.appSecret) { if (application?.gitSource?.githubApp?.privateKey) {
body.gitSource.gitlabApp.appSecret = decrypt(body.gitSource.gitlabApp.appSecret); application.gitSource.githubApp.privateKey = decrypt(
application.gitSource.githubApp.privateKey
);
} }
if (body?.gitSource?.gitlabApp?.webhookToken) { if (application?.gitSource?.gitlabApp?.appSecret) {
body.gitSource.gitlabApp.webhookToken = decrypt(body.gitSource.gitlabApp.webhookToken); application.gitSource.gitlabApp.appSecret = decrypt(
application.gitSource.gitlabApp.appSecret
);
} }
if (body?.secrets.length > 0) { if (application?.gitSource?.gitlabApp?.webhookToken) {
body.secrets = body.secrets.map((s) => { application.gitSource.gitlabApp.webhookToken = decrypt(
application.gitSource.gitlabApp.webhookToken
);
}
if (application?.secrets.length > 0) {
application.secrets = application.secrets.map((s) => {
s.value = decrypt(s.value); s.value = decrypt(s.value);
return s; return s;
}); });
} }
return { ...application };
return { ...body };
} catch (e) { } catch (e) {
throw { status: 404, body: { message: e.message } }; throw { status: 404, body: { message: e.message } };
} }
@@ -131,7 +135,8 @@ export async function getApplication({ id, teamId }) {
destinationDocker: true, destinationDocker: true,
settings: true, settings: true,
gitSource: { include: { githubApp: true, gitlabApp: true } }, gitSource: { include: { githubApp: true, gitlabApp: true } },
secrets: true secrets: true,
persistentStorage: true
} }
}); });
@@ -157,24 +162,41 @@ export async function getApplication({ id, teamId }) {
return { ...body }; return { ...body };
} }
export async function configureGitRepository({ id, repository, branch, projectId, webhookToken }) { export async function configureGitRepository({
id,
repository,
branch,
projectId,
webhookToken,
autodeploy
}) {
if (webhookToken) { if (webhookToken) {
const encryptedWebhookToken = encrypt(webhookToken); const encryptedWebhookToken = encrypt(webhookToken);
return await prisma.application.update({ await prisma.application.update({
where: { id }, where: { id },
data: { data: {
repository, repository,
branch, branch,
projectId, projectId,
gitSource: { update: { gitlabApp: { update: { webhookToken: encryptedWebhookToken } } } } gitSource: { update: { gitlabApp: { update: { webhookToken: encryptedWebhookToken } } } },
settings: { update: { autodeploy } }
} }
}); });
} else { } else {
return await prisma.application.update({ await prisma.application.update({
where: { id }, where: { id },
data: { repository, branch, projectId } data: { repository, branch, projectId, settings: { update: { autodeploy } } }
}); });
} }
if (!autodeploy) {
const applications = await prisma.application.findMany({ where: { branch, projectId } });
for (const application of applications) {
await prisma.applicationSettings.updateMany({
where: { applicationId: application.id },
data: { autodeploy: false }
});
}
}
} }
export async function configureBuildPack({ id, buildPack }) { export async function configureBuildPack({ id, buildPack }) {
@@ -209,10 +231,14 @@ export async function configureApplication({
}); });
} }
export async function setApplicationSettings({ id, debug, previews, dualCerts }) { export async function checkDoubleBranch(branch, projectId) {
const applications = await prisma.application.findMany({ where: { branch, projectId } });
return applications.length > 1;
}
export async function setApplicationSettings({ id, debug, previews, dualCerts, autodeploy }) {
return await prisma.application.update({ return await prisma.application.update({
where: { id }, where: { id },
data: { settings: { update: { debug, previews, dualCerts } } }, data: { settings: { update: { debug, previews, dualCerts, autodeploy } } },
include: { destinationDocker: true } include: { destinationDocker: true }
}); });
} }
@@ -239,3 +265,7 @@ export async function createBuild({
} }
}); });
} }
export async function getPersistentStorage(id) {
return await prisma.applicationPersistentStorage.findMany({ where: { applicationId: id } });
}

View File

@@ -107,6 +107,7 @@ export const supportedServiceTypesAndVersions = [
name: 'plausibleanalytics', name: 'plausibleanalytics',
fancyName: 'Plausible Analytics', fancyName: 'Plausible Analytics',
baseImage: 'plausible/analytics', baseImage: 'plausible/analytics',
images: ['bitnami/postgresql:13.2.0', 'yandex/clickhouse-server:21.3.2.5'],
versions: ['latest'], versions: ['latest'],
ports: { ports: {
main: 8000 main: 8000
@@ -143,6 +144,7 @@ export const supportedServiceTypesAndVersions = [
name: 'wordpress', name: 'wordpress',
fancyName: 'Wordpress', fancyName: 'Wordpress',
baseImage: 'wordpress', baseImage: 'wordpress',
images: ['bitnami/mysql:5.7'],
versions: ['latest', 'php8.1', 'php8.0', 'php7.4', 'php7.3'], versions: ['latest', 'php8.1', 'php8.0', 'php7.4', 'php7.3'],
ports: { ports: {
main: 80 main: 80
@@ -165,6 +167,34 @@ export const supportedServiceTypesAndVersions = [
ports: { ports: {
main: 8010 main: 8010
} }
},
{
name: 'n8n',
fancyName: 'n8n',
baseImage: 'n8nio/n8n',
versions: ['latest'],
ports: {
main: 5678
}
},
{
name: 'uptimekuma',
fancyName: 'Uptime Kuma',
baseImage: 'louislam/uptime-kuma',
versions: ['latest'],
ports: {
main: 3001
}
},
{
name: 'ghost',
fancyName: 'Ghost',
baseImage: 'bitnami/ghost',
images: ['bitnami/mariadb'],
versions: ['latest'],
ports: {
main: 2368
}
} }
]; ];
@@ -189,6 +219,13 @@ export function getServiceImage(type) {
} }
return ''; return '';
} }
export function getServiceImages(type) {
const found = supportedServiceTypesAndVersions.find((t) => t.name === type);
if (found) {
return found.images;
}
return [];
}
export function generateDatabaseConfiguration(database) { export function generateDatabaseConfiguration(database) {
const { const {
id, id,

View File

@@ -1,3 +1,4 @@
import { asyncExecShell, getEngine } from '$lib/common';
import { decrypt, encrypt } from '$lib/crypto'; import { decrypt, encrypt } from '$lib/crypto';
import cuid from 'cuid'; import cuid from 'cuid';
import { generatePassword } from '.'; import { generatePassword } from '.';
@@ -20,6 +21,7 @@ export async function getService({ id, teamId }) {
minio: true, minio: true,
vscodeserver: true, vscodeserver: true,
wordpress: true, wordpress: true,
ghost: true,
serviceSecret: true serviceSecret: true
} }
}); });
@@ -43,12 +45,18 @@ export async function getService({ id, teamId }) {
if (body.wordpress?.mysqlRootUserPassword) if (body.wordpress?.mysqlRootUserPassword)
body.wordpress.mysqlRootUserPassword = decrypt(body.wordpress.mysqlRootUserPassword); body.wordpress.mysqlRootUserPassword = decrypt(body.wordpress.mysqlRootUserPassword);
if (body.ghost?.mariadbPassword) body.ghost.mariadbPassword = decrypt(body.ghost.mariadbPassword);
if (body.ghost?.mariadbRootUserPassword)
body.ghost.mariadbRootUserPassword = decrypt(body.ghost.mariadbRootUserPassword);
if (body.ghost?.defaultPassword) body.ghost.defaultPassword = decrypt(body.ghost.defaultPassword);
if (body?.serviceSecret.length > 0) { if (body?.serviceSecret.length > 0) {
body.serviceSecret = body.serviceSecret.map((s) => { body.serviceSecret = body.serviceSecret.map((s) => {
s.value = decrypt(s.value); s.value = decrypt(s.value);
return s; return s;
}); });
} }
return { ...body }; return { ...body };
} }
@@ -119,6 +127,44 @@ export async function configureServiceType({ id, type }) {
type type
} }
}); });
} else if (type === 'n8n') {
await prisma.service.update({
where: { id },
data: {
type
}
});
} else if (type === 'uptimekuma') {
await prisma.service.update({
where: { id },
data: {
type
}
});
} else if (type === 'ghost') {
const defaultEmail = `${cuid()}@coolify.io`;
const defaultPassword = encrypt(generatePassword());
const mariadbUser = cuid();
const mariadbPassword = encrypt(generatePassword());
const mariadbRootUser = cuid();
const mariadbRootUserPassword = encrypt(generatePassword());
await prisma.service.update({
where: { id },
data: {
type,
ghost: {
create: {
defaultEmail,
defaultPassword,
mariadbUser,
mariadbPassword,
mariadbRootUser,
mariadbRootUserPassword
}
}
}
});
} }
} }
export async function setServiceVersion({ id, version }) { export async function setServiceVersion({ id, version }) {
@@ -139,7 +185,7 @@ export async function updatePlausibleAnalyticsService({ id, fqdn, email, usernam
await prisma.plausibleAnalytics.update({ where: { serviceId: id }, data: { email, username } }); await prisma.plausibleAnalytics.update({ where: { serviceId: id }, data: { email, username } });
await prisma.service.update({ where: { id }, data: { name, fqdn } }); await prisma.service.update({ where: { id }, data: { name, fqdn } });
} }
export async function updateNocoDbOrMinioService({ id, fqdn, name }) { export async function updateService({ id, fqdn, name }) {
return await prisma.service.update({ where: { id }, data: { fqdn, name } }); return await prisma.service.update({ where: { id }, data: { fqdn, name } });
} }
export async function updateLanguageToolService({ id, fqdn, name }) { export async function updateLanguageToolService({ id, fqdn, name }) {
@@ -160,8 +206,15 @@ export async function updateWordpress({ id, fqdn, name, mysqlDatabase, extraConf
export async function updateMinioService({ id, publicPort }) { export async function updateMinioService({ id, publicPort }) {
return await prisma.minio.update({ where: { serviceId: id }, data: { publicPort } }); return await prisma.minio.update({ where: { serviceId: id }, data: { publicPort } });
} }
export async function updateGhostService({ id, fqdn, name, mariadbDatabase }) {
return await prisma.service.update({
where: { id },
data: { fqdn, name, ghost: { update: { mariadbDatabase } } }
});
}
export async function removeService({ id }) { export async function removeService({ id }) {
await prisma.ghost.deleteMany({ where: { serviceId: id } });
await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } }); await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } });
await prisma.minio.deleteMany({ where: { serviceId: id } }); await prisma.minio.deleteMany({ where: { serviceId: id } });
await prisma.vscodeserver.deleteMany({ where: { serviceId: id } }); await prisma.vscodeserver.deleteMany({ where: { serviceId: id } });

View File

@@ -1,5 +1,6 @@
import Dockerode from 'dockerode'; import Dockerode from 'dockerode';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { checkPnpm } from './buildPacks/common';
import { saveBuildLog } from './common'; import { saveBuildLog } from './common';
export async function buildCacheImageWithNode(data, imageForBuild) { export async function buildCacheImageWithNode(data, imageForBuild) {
@@ -16,9 +17,10 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
secrets, secrets,
pullmergeRequestId pullmergeRequestId
} = data; } = data;
const isPnpm = checkPnpm(installCommand, buildCommand);
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageForBuild}`); Dockerfile.push(`FROM ${imageForBuild}`);
Dockerfile.push('WORKDIR /usr/src/app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`); Dockerfile.push(`LABEL coolify.image=true`);
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
@@ -35,20 +37,14 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
} }
}); });
} }
// TODO: If build command defined, install command should be the default yarn install 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');
}
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
if (installCommand) { if (installCommand) {
Dockerfile.push(`COPY ./${baseDirectory || ''}package*.json ./`);
try {
await fs.stat(`${workdir}/yarn.lock`);
Dockerfile.push(`COPY ./${baseDirectory || ''}yarn.lock ./`);
} catch (error) {}
try {
await fs.stat(`${workdir}/pnpm-lock.yaml`);
Dockerfile.push(`COPY ./${baseDirectory || ''}pnpm-lock.yaml ./`);
} catch (error) {}
Dockerfile.push(`RUN ${installCommand}`); Dockerfile.push(`RUN ${installCommand}`);
} }
Dockerfile.push(`COPY ./${baseDirectory || ''} ./`);
Dockerfile.push(`RUN ${buildCommand}`); Dockerfile.push(`RUN ${buildCommand}`);
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ applicationId, tag, workdir, docker, buildId, isCache: true, debug }); await buildImage({ applicationId, tag, workdir, docker, buildId, isCache: true, debug });
@@ -69,14 +65,14 @@ export async function buildCacheImageWithCargo(data, imageForBuild) {
} = data; } = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageForBuild} as planner-${applicationId}`); Dockerfile.push(`FROM ${imageForBuild} as planner-${applicationId}`);
Dockerfile.push('WORKDIR /usr/src/app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push('RUN cargo install cargo-chef'); Dockerfile.push('RUN cargo install cargo-chef');
Dockerfile.push('COPY . .'); Dockerfile.push('COPY . .');
Dockerfile.push('RUN cargo chef prepare --recipe-path recipe.json'); Dockerfile.push('RUN cargo chef prepare --recipe-path recipe.json');
Dockerfile.push(`FROM ${imageForBuild}`); Dockerfile.push(`FROM ${imageForBuild}`);
Dockerfile.push('WORKDIR /usr/src/app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push('RUN cargo install cargo-chef'); Dockerfile.push('RUN cargo install cargo-chef');
Dockerfile.push(`COPY --from=planner-${applicationId} /usr/src/app/recipe.json recipe.json`); Dockerfile.push(`COPY --from=planner-${applicationId} /app/recipe.json recipe.json`);
Dockerfile.push('RUN cargo chef cook --release --recipe-path recipe.json'); Dockerfile.push('RUN cargo chef cook --release --recipe-path recipe.json');
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ applicationId, tag, workdir, docker, buildId, isCache: true, debug }); await buildImage({ applicationId, tag, workdir, docker, buildId, isCache: true, debug });

View File

@@ -82,7 +82,7 @@ backend backend-certbot
# updatedAt={{updatedAt}} # updatedAt={{updatedAt}}
backend {{domain}} backend {{domain}}
option forwardfor option forwardfor
server {{id}} {{id}}:{{port}} check server {{id}} {{id}}:{{port}}
{{/isRunning}} {{/isRunning}}
{{/applications}} {{/applications}}
@@ -91,7 +91,7 @@ backend {{domain}}
# updatedAt={{updatedAt}} # updatedAt={{updatedAt}}
backend {{domain}} backend {{domain}}
option forwardfor option forwardfor
server {{id}} {{id}}:{{port}} check server {{id}} {{id}}:{{port}}
{{/isRunning}} {{/isRunning}}
{{/services}} {{/services}}

View File

@@ -45,6 +45,7 @@ export default async function ({
const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`); const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`);
return commit.replace('\n', ''); return commit.replace('\n', '');
} catch (error) { } catch (error) {
console.log({ error });
return ErrorHandler(error); return ErrorHandler(error);
} }
} }

View File

@@ -99,37 +99,44 @@ export async function letsEncrypt(domain, id = null, isCoolify = false) {
export async function generateSSLCerts() { export async function generateSSLCerts() {
const ssls = []; const ssls = [];
const applications = await db.prisma.application.findMany({ const applications = await db.prisma.application.findMany({
include: { destinationDocker: true, settings: true } include: { destinationDocker: true, settings: true },
orderBy: { createdAt: 'desc' }
}); });
for (const application of applications) { for (const application of applications) {
const { try {
fqdn, if (application.fqdn && application.destinationDockerId) {
id, const {
destinationDocker: { engine, network }, fqdn,
settings: { previews } id,
} = application; destinationDocker: { engine, network },
const isRunning = await checkContainer(engine, id); settings: { previews }
const domain = getDomain(fqdn); } = application;
const isHttps = fqdn.startsWith('https://'); const isRunning = await checkContainer(engine, id);
if (isRunning) { const domain = getDomain(fqdn);
if (isHttps) ssls.push({ domain, id, isCoolify: false }); const isHttps = fqdn.startsWith('https://');
} if (isRunning) {
if (previews) { if (isHttps) ssls.push({ domain, id, isCoolify: false });
const host = getEngine(engine); }
const { stdout } = await asyncExecShell( if (previews) {
`DOCKER_HOST=${host} docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"` const host = getEngine(engine);
); const { stdout } = await asyncExecShell(
const containers = stdout `DOCKER_HOST=${host} docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"`
.trim() );
.split('\n') const containers = stdout
.filter((a) => a) .trim()
.map((c) => c.replace(/"/g, '')); .split('\n')
if (containers.length > 0) { .filter((a) => a)
for (const container of containers) { .map((c) => c.replace(/"/g, ''));
let previewDomain = `${container.split('-')[1]}.${domain}`; if (containers.length > 0) {
if (isHttps) ssls.push({ domain: previewDomain, id, isCoolify: false }); for (const container of containers) {
let previewDomain = `${container.split('-')[1]}.${domain}`;
if (isHttps) ssls.push({ domain: previewDomain, id, isCoolify: false });
}
}
} }
} }
} catch (error) {
console.log(`Error during generateSSLCerts with ${application.fqdn}: ${error}`);
} }
} }
const services = await db.prisma.service.findMany({ const services = await db.prisma.service.findMany({
@@ -138,25 +145,33 @@ export async function generateSSLCerts() {
minio: true, minio: true,
plausibleAnalytics: true, plausibleAnalytics: true,
vscodeserver: true, vscodeserver: true,
wordpress: true wordpress: true,
} ghost: true
},
orderBy: { createdAt: 'desc' }
}); });
for (const service of services) { for (const service of services) {
const { try {
fqdn, if (service.fqdn && service.destinationDockerId) {
id, const {
type, fqdn,
destinationDocker: { engine } id,
} = service; type,
const found = db.supportedServiceTypesAndVersions.find((a) => a.name === type); destinationDocker: { engine }
if (found) { } = service;
const domain = getDomain(fqdn); const found = db.supportedServiceTypesAndVersions.find((a) => a.name === type);
const isHttps = fqdn.startsWith('https://'); if (found) {
const isRunning = await checkContainer(engine, id); const domain = getDomain(fqdn);
if (isRunning) { const isHttps = fqdn.startsWith('https://');
if (isHttps) ssls.push({ domain, id, isCoolify: false }); const isRunning = await checkContainer(engine, id);
if (isRunning) {
if (isHttps) ssls.push({ domain, id, isCoolify: false });
}
}
} }
} catch (error) {
console.log(`Error during generateSSLCerts with ${service.fqdn}: ${error}`);
} }
} }
const { fqdn } = await db.prisma.setting.findFirst(); const { fqdn } = await db.prisma.setting.findFirst();

View File

@@ -3,7 +3,14 @@ import fs from 'fs/promises';
import * as buildpacks from '../buildPacks'; import * as buildpacks from '../buildPacks';
import * as importers from '../importers'; import * as importers from '../importers';
import { dockerInstance } from '../docker'; import { dockerInstance } from '../docker';
import { asyncExecShell, createDirectories, getDomain, getEngine, saveBuildLog } from '../common'; import {
asyncExecShell,
asyncSleep,
createDirectories,
getDomain,
getEngine,
saveBuildLog
} from '../common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { decrypt } from '$lib/crypto'; import { decrypt } from '$lib/crypto';
import { sentry } from '$lib/common'; import { sentry } from '$lib/common';
@@ -12,7 +19,7 @@ import {
makeLabelForStandaloneApplication, makeLabelForStandaloneApplication,
setDefaultConfiguration setDefaultConfiguration
} from '$lib/buildPacks/common'; } from '$lib/buildPacks/common';
import { letsEncrypt } from '$lib/letsencrypt'; import yaml from 'js-yaml';
export default async function (job) { export default async function (job) {
/* /*
@@ -39,17 +46,33 @@ export default async function (job) {
publishDirectory, publishDirectory,
projectId, projectId,
secrets, secrets,
phpModules,
type, type,
pullmergeRequestId = null, pullmergeRequestId = null,
sourceBranch = null, sourceBranch = null,
settings settings,
persistentStorage
} = job.data; } = job.data;
const { debug } = settings; const { debug } = settings;
await asyncSleep(500);
await db.prisma.build.updateMany({
where: {
status: 'queued',
id: { not: buildId },
applicationId,
createdAt: { lt: new Date(new Date().getTime() - 60 * 60 * 1000) }
},
data: { status: 'failed' }
});
let imageId = applicationId; let imageId = applicationId;
let domain = getDomain(fqdn); let domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://'); let volumes =
persistentStorage?.map((storage) => {
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${
buildPack !== 'docker' ? '/app' : ''
}${storage.path}`;
}) || [];
// Previews, we need to get the source branch and set subdomain // Previews, we need to get the source branch and set subdomain
if (pullmergeRequestId) { if (pullmergeRequestId) {
branch = sourceBranch; branch = sourceBranch;
@@ -67,17 +90,8 @@ export default async function (job) {
const docker = dockerInstance({ destinationDocker }); const docker = dockerInstance({ destinationDocker });
const host = getEngine(destinationDocker.engine); const host = getEngine(destinationDocker.engine);
const build = await db.createBuild({ await db.prisma.build.update({ where: { id: buildId }, data: { status: 'running' } });
id: buildId, const { workdir, repodir } = await createDirectories({ repository, buildId });
applicationId,
destinationDockerId: destinationDocker.id,
gitSourceId: gitSource.id,
githubAppId: gitSource.githubApp?.id,
gitlabAppId: gitSource.gitlabApp?.id,
type
});
const { workdir, repodir } = await createDirectories({ repository, buildId: build.id });
const configuration = await setDefaultConfiguration(job.data); const configuration = await setDefaultConfiguration(job.data);
@@ -87,6 +101,7 @@ export default async function (job) {
startCommand = configuration.startCommand; startCommand = configuration.startCommand;
buildCommand = configuration.buildCommand; buildCommand = configuration.buildCommand;
publishDirectory = configuration.publishDirectory; publishDirectory = configuration.publishDirectory;
baseDirectory = configuration.baseDirectory;
let commit = await importers[gitSource.type]({ let commit = await importers[gitSource.type]({
applicationId, applicationId,
@@ -97,19 +112,22 @@ export default async function (job) {
gitlabAppId: gitSource.gitlabApp?.id, gitlabAppId: gitSource.gitlabApp?.id,
repository, repository,
branch, branch,
buildId: build.id, buildId,
apiUrl: gitSource.apiUrl, apiUrl: gitSource.apiUrl,
projectId, projectId,
deployKeyId: gitSource.gitlabApp?.deployKeyId || null, deployKeyId: gitSource.gitlabApp?.deployKeyId || null,
privateSshKey: decrypt(gitSource.gitlabApp?.privateSshKey) || null privateSshKey: decrypt(gitSource.gitlabApp?.privateSshKey) || null
}); });
if (!commit) {
throw new Error('No commit found?');
}
let tag = commit.slice(0, 7); let tag = commit.slice(0, 7);
if (pullmergeRequestId) { if (pullmergeRequestId) {
tag = `${commit.slice(0, 7)}-${pullmergeRequestId}`; tag = `${commit.slice(0, 7)}-${pullmergeRequestId}`;
} }
try { try {
await db.prisma.build.update({ where: { id: build.id }, data: { commit } }); db.prisma.build.update({ where: { id: buildId }, data: { commit } });
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
@@ -160,7 +178,7 @@ export default async function (job) {
await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId); await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId);
if (buildpacks[buildPack]) if (buildpacks[buildPack])
await buildpacks[buildPack]({ await buildpacks[buildPack]({
buildId: build.id, buildId,
applicationId, applicationId,
domain, domain,
name, name,
@@ -181,7 +199,8 @@ export default async function (job) {
buildCommand, buildCommand,
startCommand, startCommand,
baseDirectory, baseDirectory,
secrets secrets,
phpModules
}); });
else { else {
saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId }); saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId });
@@ -241,14 +260,53 @@ export default async function (job) {
} }
try { try {
saveBuildLog({ line: 'Deployment started.', buildId, applicationId }); saveBuildLog({ line: 'Deployment started.', buildId, applicationId });
const { stderr } = await asyncExecShell( // for await (const volume of volumes) {
`DOCKER_HOST=${host} docker run ${envFound && `--env-file=${workdir}/.env`} ${labels.join( // const id = volume.split(':')[0];
' ' // try {
)} --name ${imageId} --network ${ // await asyncExecShell(`DOCKER_HOST=${host} docker volume inspect ${id}`);
docker.network // } catch (error) {
} --restart always -d ${applicationId}:${tag}` // await asyncExecShell(`DOCKER_HOST=${host} docker volume create ${id}`);
// }
// }
const composeVolumes = volumes.map((volume) => {
return {
[`${volume.split(':')[0]}`]: {
name: volume.split(':')[0]
}
};
});
const compose = {
version: '3.8',
services: {
[imageId]: {
image: `${applicationId}:${tag}`,
container_name: imageId,
volumes,
env_file: envFound ? [`${workdir}/.env`] : [],
networks: [docker.network],
labels: labels,
depends_on: [],
restart: 'always'
}
},
networks: {
[docker.network]: {
external: true
}
},
volumes: Object.assign({}, ...composeVolumes)
};
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(compose));
await asyncExecShell(
`DOCKER_HOST=${host} docker compose --project-directory ${workdir} up -d`
); );
if (stderr) console.log(stderr);
// const { stderr } = await asyncExecShell(
// `DOCKER_HOST=${host} docker run ${envFound && `--env-file=${workdir}/.env`} ${labels.join(
// ' '
// )} --name ${imageId} --network ${docker.network} --restart always ${volumes.length > 0 ? volumes : ''
// } -d ${applicationId}:${tag}`
// );
saveBuildLog({ line: 'Deployment successful!', buildId, applicationId }); saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
} catch (error) { } catch (error) {
saveBuildLog({ line: error, buildId, applicationId }); saveBuildLog({ line: error, buildId, applicationId });

View File

@@ -24,7 +24,7 @@ export default async function () {
console.log(error); console.log(error);
} }
try { try {
await asyncExecShell(`DOCKER_HOST=${host} docker image prune -f`); await asyncExecShell(`DOCKER_HOST=${host} docker image prune -f --filter "until=2h"`);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }

View File

@@ -87,7 +87,7 @@ const cron = async () => {
await queue.proxy.add('proxy', {}, { repeat: { every: 10000 } }); await queue.proxy.add('proxy', {}, { repeat: { every: 10000 } });
await queue.ssl.add('ssl', {}, { repeat: { every: dev ? 10000 : 60000 } }); await queue.ssl.add('ssl', {}, { repeat: { every: dev ? 10000 : 60000 } });
await queue.cleanup.add('cleanup', {}, { repeat: { every: dev ? 10000 : 300000 } }); if (!dev) await queue.cleanup.add('cleanup', {}, { repeat: { every: 300000 } });
await queue.sslRenew.add('sslRenew', {}, { repeat: { every: 1800000 } }); await queue.sslRenew.add('sslRenew', {}, { repeat: { every: 1800000 } });
const events = { const events = {
@@ -110,18 +110,18 @@ cron().catch((error) => {
const buildQueueName = 'build_queue'; const buildQueueName = 'build_queue';
const buildQueue = new Queue(buildQueueName, connectionOptions); const buildQueue = new Queue(buildQueueName, connectionOptions);
const buildWorker = new Worker(buildQueueName, async (job) => await builder(job), { const buildWorker = new Worker(buildQueueName, async (job) => await builder(job), {
concurrency: 2, concurrency: 1,
...connectionOptions ...connectionOptions
}); });
buildWorker.on('completed', async (job: Bullmq.Job) => { buildWorker.on('completed', async (job: Bullmq.Job) => {
try { try {
await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'success' } }); await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'success' } });
} catch (err) { } catch (error) {
console.log(err); console.log(error);
} finally { } finally {
const workdir = `/tmp/build-sources/${job.data.repository}/`; const workdir = `/tmp/build-sources/${job.data.repository}/${job.data.build_id}`;
await asyncExecShell(`rm -fr ${workdir}`); if (!dev) await asyncExecShell(`rm -fr ${workdir}`);
} }
return; return;
}); });
@@ -133,7 +133,7 @@ buildWorker.on('failed', async (job: Bullmq.Job, failedReason) => {
console.log(error); console.log(error);
} finally { } finally {
const workdir = `/tmp/build-sources/${job.data.repository}`; const workdir = `/tmp/build-sources/${job.data.repository}`;
await asyncExecShell(`rm -fr ${workdir}`); if (!dev) await asyncExecShell(`rm -fr ${workdir}`);
} }
saveBuildLog({ saveBuildLog({
line: 'Failed to deploy!', line: 'Failed to deploy!',

View File

@@ -4,6 +4,7 @@ export default async function () {
try { try {
return await generateSSLCerts(); return await generateSSLCerts();
} catch (error) { } catch (error) {
console.log(error);
throw error; throw error;
} }
} }

View File

@@ -76,6 +76,7 @@
import { del, post } from '$lib/api'; import { del, post } from '$lib/api';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { gitTokens } from '$lib/store'; import { gitTokens } from '$lib/store';
import { toast } from '@zerodevx/svelte-toast';
if (githubToken) $gitTokens.githubToken = githubToken; if (githubToken) $gitTokens.githubToken = githubToken;
if (gitlabToken) $gitTokens.gitlabToken = gitlabToken; if (gitlabToken) $gitTokens.gitlabToken = gitlabToken;
@@ -86,7 +87,15 @@
async function handleDeploySubmit() { async function handleDeploySubmit() {
try { try {
const { buildId } = await post(`/applications/${id}/deploy.json`, { ...application }); const { buildId } = await post(`/applications/${id}/deploy.json`, { ...application });
return await goto(`/applications/${id}/logs/build?buildId=${buildId}`); toast.push('Deployment queued.');
console.log($page.url);
if ($page.url.pathname.startsWith(`/applications/${id}/logs/build`)) {
return window.location.assign(`/applications/${id}/logs/build?buildId=${buildId}`);
} else {
return await goto(`/applications/${id}/logs/build?buildId=${buildId}`, {
replaceState: true
});
}
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
} }
@@ -108,11 +117,9 @@
try { try {
loading = true; loading = true;
await post(`/applications/${id}/stop.json`, {}); await post(`/applications/${id}/stop.json`, {});
isRunning = false; return window.location.reload();
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
} finally {
loading = false;
} }
} }
</script> </script>
@@ -271,6 +278,35 @@
</svg></button </svg></button
></a ></a
> >
<a
href="/applications/{id}/storage"
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/applications/${id}/storage`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/storage`}
>
<button
title="Persistent Storage"
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
data-tooltip="Persistent Storage"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<ellipse cx="12" cy="6" rx="8" ry="3" />
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
</svg>
</button></a
>
<a <a
href="/applications/{id}/previews" href="/applications/{id}/previews"
sveltekit:prefetch sveltekit:prefetch

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
export let application; export let application;
import Select from 'svelte-select';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
@@ -25,14 +25,19 @@
let selected = { let selected = {
projectId: undefined, projectId: undefined,
repository: undefined, repository: undefined,
branch: undefined branch: undefined,
autodeploy: application.settings.autodeploy || true
}; };
let showSave = false; let showSave = false;
async function loadRepositoriesByPage(page = 0) { async function loadRepositoriesByPage(page = 0) {
return await get(`${apiUrl}/installation/repositories?per_page=100&page=${page}`, { return await get(`${apiUrl}/installation/repositories?per_page=100&page=${page}`, {
Authorization: `token ${$gitTokens.githubToken}` Authorization: `token ${$gitTokens.githubToken}`
}); });
} }
let reposSelectOptions;
let branchSelectOptions;
async function loadRepositories() { async function loadRepositories() {
let page = 1; let page = 1;
let reposCount = 0; let reposCount = 0;
@@ -47,8 +52,13 @@
} }
} }
loading.repositories = false; loading.repositories = false;
reposSelectOptions = repositories.map((repo) => ({
value: repo.full_name,
label: repo.name
}));
} }
async function loadBranches() { async function loadBranches(event) {
selected.repository = event.detail.value;
loading.branches = true; loading.branches = true;
selected.branch = undefined; selected.branch = undefined;
selected.projectId = repositories.find((repo) => repo.full_name === selected.repository).id; selected.projectId = repositories.find((repo) => repo.full_name === selected.repository).id;
@@ -56,6 +66,10 @@
branches = await get(`${apiUrl}/repos/${selected.repository}/branches`, { branches = await get(`${apiUrl}/repos/${selected.repository}/branches`, {
Authorization: `token ${$gitTokens.githubToken}` Authorization: `token ${$gitTokens.githubToken}`
}); });
branchSelectOptions = branches.map((branch) => ({
value: branch.name,
label: branch.name
}));
return; return;
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
@@ -63,13 +77,21 @@
loading.branches = false; loading.branches = false;
} }
} }
async function isBranchAlreadyUsed() { async function isBranchAlreadyUsed(event) {
selected.branch = event.detail.value;
try { try {
const data = await get( const data = await get(
`/applications/${id}/configuration/repository.json?repository=${selected.repository}&branch=${selected.branch}` `/applications/${id}/configuration/repository.json?repository=${selected.repository}&branch=${selected.branch}`
); );
if (data.used) { if (data.used) {
errorNotification('This branch is already used by another application.'); const sure = confirm(
`This branch is already used by another application. Webhooks won't work in this case for both applications. Are you sure you want to use it?`
);
if (sure) {
selected.autodeploy = false;
showSave = true;
return true;
}
showSave = false; showSave = false;
return true; return true;
} }
@@ -142,49 +164,35 @@
<a href={`/sources/${application.gitSource.id}`}><button>Configure it now</button></a> <a href={`/sources/${application.gitSource.id}`}><button>Configure it now</button></a>
</div> </div>
{:else} {:else}
<form on:submit|preventDefault={handleSubmit}> <form on:submit|preventDefault={handleSubmit} class="flex flex-col justify-center text-center">
<div> <div class="flex-col space-y-3 md:space-y-0 space-x-1">
{#if loading.repositories} <div class="flex gap-4">
<select name="repository" disabled class="w-96"> <div class="custom-select-wrapper">
<option selected value="">Loading repositories...</option> <Select
</select> placeholder={loading.repositories
{:else} ? 'Loading repositories ...'
<select : 'Please select a repository'}
name="repository" id="repository"
class="w-96" on:select={loadBranches}
bind:value={selected.repository} items={reposSelectOptions}
on:change={loadBranches} isDisabled={loading.repositories}
> />
<option value="" disabled selected>Please select a repository</option> </div>
{#each repositories as repository} <input class="hidden" bind:value={selected.projectId} name="projectId" />
<option value={repository.full_name}>{repository.name}</option> <div class="custom-select-wrapper">
{/each} <Select
</select> placeholder={loading.branches
{/if} ? 'Loading branches ...'
<input class="hidden" bind:value={selected.projectId} name="projectId" /> : !selected.repository
{#if loading.branches} ? 'Please select a repository first'
<select name="branch" disabled class="w-96"> : 'Please select a branch'}
<option selected value="">Loading branches...</option> id="repository"
</select> on:select={isBranchAlreadyUsed}
{:else} items={branchSelectOptions}
<select isDisabled={loading.branches || !selected.repository}
name="branch" />
class="w-96" </div>
disabled={!selected.repository} </div>
bind:value={selected.branch}
on:change={isBranchAlreadyUsed}
>
{#if !selected.repository}
<option value="" disabled selected>Select a repository first</option>
{:else}
<option value="" disabled selected>Please select a branch</option>
{/if}
{#each branches as branch}
<option value={branch.name}>{branch.name}</option>
{/each}
</select>
{/if}
</div> </div>
<div class="pt-5 flex-col flex justify-center items-center space-y-4"> <div class="pt-5 flex-col flex justify-center items-center space-y-4">
<button <button

View File

@@ -30,6 +30,7 @@
let projects = []; let projects = [];
let branches = []; let branches = [];
let showSave = false; let showSave = false;
let autodeploy = application.settings.autodeploy || true;
let selected = { let selected = {
group: undefined, group: undefined,
@@ -138,7 +139,14 @@
`/applications/${id}/configuration/repository.json?repository=${selected.project.path_with_namespace}&branch=${selected.branch.name}` `/applications/${id}/configuration/repository.json?repository=${selected.project.path_with_namespace}&branch=${selected.branch.name}`
); );
if (data.used) { if (data.used) {
errorNotification('This branch is already used by another application.'); const sure = confirm(
`This branch is already used by another application. Webhooks won't work in this case for both applications. Are you sure you want to use it?`
);
if (sure) {
autodeploy = false;
showSave = true;
return true;
}
showSave = false; showSave = false;
return true; return true;
} }
@@ -235,10 +243,14 @@
const url = `/applications/${id}/configuration/repository.json`; const url = `/applications/${id}/configuration/repository.json`;
try { try {
const repository = `${selected.group.full_path.replace('-personal', '')}/${
selected.project.name
}`;
await post(url, { await post(url, {
repository: `${selected.group.full_path}/${selected.project.name}`, repository,
branch: selected.branch.name, branch: selected.branch.name,
projectId: selected.project.id, projectId: selected.project.id,
autodeploy,
webhookToken webhookToken
}); });
return await goto(from || `/applications/${id}/configuration/buildpack`); return await goto(from || `/applications/${id}/configuration/buildpack`);

View File

@@ -30,14 +30,21 @@ export const post: RequestHandler = async (event) => {
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { id } = event.params; const { id } = event.params;
let { repository, branch, projectId, webhookToken } = await event.request.json(); let { repository, branch, projectId, webhookToken, autodeploy } = await event.request.json();
repository = repository.toLowerCase(); repository = repository.toLowerCase();
branch = branch.toLowerCase(); branch = branch.toLowerCase();
projectId = Number(projectId); projectId = Number(projectId);
try { try {
await db.configureGitRepository({ id, repository, branch, projectId, webhookToken }); await db.configureGitRepository({
id,
repository,
branch,
projectId,
webhookToken,
autodeploy
});
return { status: 201 }; return { status: 201 };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);

View File

@@ -41,7 +41,7 @@
gitlabApp: Prisma.GitlabApp; gitlabApp: Prisma.GitlabApp;
githubApp: Prisma.GithubApp; githubApp: Prisma.GithubApp;
}; };
sources = sources.filter( const filteredSources = sources.filter(
(source) => (source) =>
(source.type === 'github' && source.githubAppId && source.githubApp.installationId) || (source.type === 'github' && source.githubAppId && source.githubApp.installationId) ||
(source.type === 'gitlab' && source.gitlabAppId) (source.type === 'gitlab' && source.gitlabAppId)
@@ -59,8 +59,8 @@
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Select a Git Source</div> <div class="mr-4 text-2xl tracking-tight">Select a Git Source</div>
</div> </div>
<div class="flex justify-center"> <div class="flex flex-col justify-center">
{#if !sources || sources.length === 0} {#if !filteredSources || filteredSources.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="pb-2">No configurable Git Source found</div> <div class="pb-2">No configurable Git Source found</div>
<div class="flex justify-center"> <div class="flex justify-center">
@@ -83,7 +83,7 @@
</div> </div>
{:else} {:else}
<div class="flex flex-wrap justify-center"> <div class="flex flex-wrap justify-center">
{#each sources as source} {#each filteredSources as source}
<div class="p-2"> <div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(source.id)}> <form on:submit|preventDefault={() => handleSubmit(source.id)}>
<button <button

View File

@@ -11,6 +11,7 @@ export const post: RequestHandler = async (event) => {
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { id } = event.params; const { id } = event.params;
const { pullmergeRequestId = null, branch } = await event.request.json();
try { try {
const buildId = cuid(); const buildId = cuid();
const applicationFound = await db.getApplication({ id, teamId }); const applicationFound = await db.getApplication({ id, teamId });
@@ -30,7 +31,29 @@ export const post: RequestHandler = async (event) => {
await db.prisma.application.update({ where: { id }, data: { configHash } }); await db.prisma.application.update({ where: { id }, data: { configHash } });
} }
await db.prisma.application.update({ where: { id }, data: { updatedAt: new Date() } }); await db.prisma.application.update({ where: { id }, data: { updatedAt: new Date() } });
await buildQueue.add(buildId, { build_id: buildId, type: 'manual', ...applicationFound }); await db.prisma.build.create({
data: {
id: buildId,
applicationId: id,
destinationDockerId: applicationFound.destinationDocker.id,
gitSourceId: applicationFound.gitSource.id,
githubAppId: applicationFound.gitSource.githubApp?.id,
gitlabAppId: applicationFound.gitSource.gitlabApp?.id,
status: 'queued',
type: 'manual'
}
});
if (pullmergeRequestId) {
await buildQueue.add(buildId, {
build_id: buildId,
type: 'manual',
...applicationFound,
sourceBranch: branch,
pullmergeRequestId
});
} else {
await buildQueue.add(buildId, { build_id: buildId, type: 'manual', ...applicationFound });
}
return { return {
status: 200, status: 200,
body: { body: {

View File

@@ -56,7 +56,7 @@
let debug = application.settings.debug; let debug = application.settings.debug;
let previews = application.settings.previews; let previews = application.settings.previews;
let dualCerts = application.settings.dualCerts; let dualCerts = application.settings.dualCerts;
let autodeploy = application.settings.autodeploy;
if (browser && window.location.hostname === 'demo.coolify.io' && !application.fqdn) { if (browser && window.location.hostname === 'demo.coolify.io' && !application.fqdn) {
application.fqdn = `http://${cuid()}.demo.coolify.io`; application.fqdn = `http://${cuid()}.demo.coolify.io`;
} }
@@ -75,10 +75,32 @@
if (name === 'dualCerts') { if (name === 'dualCerts') {
dualCerts = !dualCerts; dualCerts = !dualCerts;
} }
if (name === 'autodeploy') {
autodeploy = !autodeploy;
}
try { try {
await post(`/applications/${id}/settings.json`, { previews, debug, dualCerts }); await post(`/applications/${id}/settings.json`, {
previews,
debug,
dualCerts,
autodeploy,
branch: application.branch,
projectId: application.projectId
});
return toast.push('Settings saved.'); return toast.push('Settings saved.');
} catch ({ error }) { } catch ({ error }) {
if (name === 'debug') {
debug = !debug;
}
if (name === 'previews') {
previews = !previews;
}
if (name === 'dualCerts') {
dualCerts = !dualCerts;
}
if (name === 'autodeploy') {
autodeploy = !autodeploy;
}
return errorNotification(error); return errorNotification(error);
} }
} }
@@ -87,7 +109,7 @@
try { try {
await post(`/applications/${id}/check.json`, { fqdn: application.fqdn, forceSave }); await post(`/applications/${id}/check.json`, { fqdn: application.fqdn, forceSave });
await post(`/applications/${id}.json`, { ...application }); await post(`/applications/${id}.json`, { ...application });
return window.location.reload(); return toast.push('Configurations saved.');
} catch ({ error }) { } catch ({ error }) {
if (error.startsWith('DNS not set')) { if (error.startsWith('DNS not set')) {
forceSave = true; forceSave = true;
@@ -340,7 +362,6 @@
/> />
</div> </div>
{/if} {/if}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<div class="flex-col"> <div class="flex-col">
<label for="baseDirectory" class="pt-2 text-base font-bold text-stone-100" <label for="baseDirectory" class="pt-2 text-base font-bold text-stone-100"
@@ -383,22 +404,23 @@
<div class="flex space-x-1 pb-5 font-bold"> <div class="flex space-x-1 pb-5 font-bold">
<div class="title">Features</div> <div class="title">Features</div>
</div> </div>
<!-- <ul class="mt-2 divide-y divide-stone-800">
<Setting
bind:setting={forceSSL}
on:click={() => changeSettings('forceSSL')}
title="Force https"
description="Creates a https redirect for all requests from http and also generates a https certificate for the domain through Let's Encrypt."
/>
</ul> -->
<div class="px-10 pb-10"> <div class="px-10 pb-10">
<div class="grid grid-cols-2 items-center">
<Setting
isCenter={false}
bind:setting={autodeploy}
on:click={() => changeSettings('autodeploy')}
title="Enable Automatic Deployment"
description="Enable automatic deployment through webhooks."
/>
</div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
isCenter={false} isCenter={false}
bind:setting={previews} bind:setting={previews}
on:click={() => changeSettings('previews')} on:click={() => changeSettings('previews')}
title="Enable MR/PR Previews" title="Enable MR/PR Previews"
description="Creates previews from pull and merge requests." description="Enable preview deployments from pull or merge requests."
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">

View File

@@ -43,11 +43,11 @@
logs = logs.concat(responseLogs.map((log) => ({ ...log, line: cleanAnsiCodes(log.line) }))); logs = logs.concat(responseLogs.map((log) => ({ ...log, line: cleanAnsiCodes(log.line) })));
loading = false; loading = false;
streamInterval = setInterval(async () => { streamInterval = setInterval(async () => {
if (status !== 'running') { if (status !== 'running' && status !== 'queued') {
clearInterval(streamInterval); clearInterval(streamInterval);
return; return;
} }
const nextSequence = logs[logs.length - 1].time; const nextSequence = logs[logs.length - 1]?.time || 0;
try { try {
const data = await get( const data = await get(
`/applications/${id}/logs/build/build.json?buildId=${buildId}&sequence=${nextSequence}` `/applications/${id}/logs/build/build.json?buildId=${buildId}&sequence=${nextSequence}`
@@ -83,38 +83,42 @@
{#if currentStatus === 'running'} {#if currentStatus === 'running'}
<LoadingLogs /> <LoadingLogs />
{/if} {/if}
<div class="flex justify-end sticky top-0 p-2"> {#if currentStatus === 'queued'}
<button <div class="text-center font-bold text-xl">Queued and waiting for execution.</div>
on:click={followBuild} {:else}
class="bg-transparent" <div class="flex justify-end sticky top-0 p-2">
data-tooltip="Follow logs" <button
class:text-green-500={followingBuild} on:click={followBuild}
> class="bg-transparent"
<svg data-tooltip="Follow logs"
xmlns="http://www.w3.org/2000/svg" class:text-green-500={followingBuild}
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" /> <svg
<circle cx="12" cy="12" r="9" /> xmlns="http://www.w3.org/2000/svg"
<line x1="8" y1="12" x2="12" y2="16" /> class="w-6 h-6"
<line x1="12" y1="8" x2="12" y2="16" /> viewBox="0 0 24 24"
<line x1="16" y1="12" x2="12" y2="16" /> stroke-width="1.5"
</svg> stroke="currentColor"
</button> fill="none"
</div> stroke-linecap="round"
<div stroke-linejoin="round"
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} <path stroke="none" d="M0 0h24v24H0z" fill="none" />
> <circle cx="12" cy="12" r="9" />
{#each logs as log} <line x1="8" y1="12" x2="12" y2="16" />
<div>{log.line + '\n'}</div> <line x1="12" y1="8" x2="12" y2="16" />
{/each} <line x1="16" y1="12" x2="12" y2="16" />
</div> </svg>
</button>
</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}
>
{#each logs as log}
<div>{log.line + '\n'}</div>
{/each}
</div>
{/if}
</div> </div>
{/if} {/if}

View File

@@ -19,7 +19,7 @@ export const get: RequestHandler = async (event) => {
return { return {
body: { body: {
logs, logs,
status: data?.status || 'running' status: data?.status
} }
}; };
} catch (error) { } catch (error) {

View File

@@ -54,7 +54,6 @@
return build; return build;
}); });
return window.location.reload();
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
} }
@@ -121,6 +120,8 @@
<div class="w-48 text-center text-xs"> <div class="w-48 text-center text-xs">
{#if build.status === 'running'} {#if build.status === 'running'}
<div class="font-bold">Running</div> <div class="font-bold">Running</div>
{:else if build.status === 'queued'}
<div class="font-bold">Queued</div>
{:else} {:else}
<div>{build.since}</div> <div>{build.since}</div>
<div>Finished in <span class="font-bold">{build.took}s</span></div> <div>Finished in <span class="font-bold">{build.took}s</span></div>

View File

@@ -26,15 +26,28 @@
export let applicationSecrets; export let applicationSecrets;
import { getDomain } from '$lib/components/common'; import { getDomain } from '$lib/components/common';
import Secret from '../secrets/_Secret.svelte'; import Secret from '../secrets/_Secret.svelte';
import { get } from '$lib/api'; import { get, post } from '$lib/api';
import { page } from '$app/stores'; import { page } from '$app/stores';
import Explainer from '$lib/components/Explainer.svelte'; import Explainer from '$lib/components/Explainer.svelte';
import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast';
const { id } = $page.params; const { id } = $page.params;
async function refreshSecrets() { async function refreshSecrets() {
const data = await get(`/applications/${id}/secrets.json`); const data = await get(`/applications/${id}/secrets.json`);
PRMRSecrets = [...data.secrets]; PRMRSecrets = [...data.secrets];
} }
async function redeploy(container) {
try {
await post(`/applications/${id}/deploy.json`, {
pullmergeRequestId: container.pullmergeRequestId,
branch: container.branch
});
toast.push('Application redeployed queued.');
} catch ({ error }) {
return errorNotification(error);
}
}
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
@@ -90,6 +103,11 @@
<div class="truncate text-center text-xl font-bold">{getDomain(container.fqdn)}</div> <div class="truncate text-center text-xl font-bold">{getDomain(container.fqdn)}</div>
</div> </div>
</a> </a>
<div class="flex items-center justify-center">
<button class="bg-coollabs hover:bg-coollabs-100" on:click={() => redeploy(container)}
>Redeploy</button
>
</div>
{/each} {/each}
{:else} {:else}
<div class="flex-col"> <div class="flex-col">

View File

@@ -8,10 +8,17 @@ export const post: RequestHandler = async (event) => {
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { id } = event.params; const { id } = event.params;
const { debug, previews, dualCerts } = await event.request.json(); const { debug, previews, dualCerts, autodeploy, branch, projectId } = await event.request.json();
try { try {
await db.setApplicationSettings({ id, debug, previews, dualCerts }); const isDouble = await db.checkDoubleBranch(branch, projectId);
if (isDouble && autodeploy) {
throw {
message:
'Cannot activate automatic deployments until only one application is defined for this repository / branch.'
};
}
await db.setApplicationSettings({ id, debug, previews, dualCerts, autodeploy });
return { status: 201 }; return { status: 201 };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);

View File

@@ -0,0 +1,73 @@
<script lang="ts">
export let isNew = false;
export let storage = {
id: null,
path: null
};
import { del, post } from '$lib/api';
import { page } from '$app/stores';
import { createEventDispatcher } from 'svelte';
import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast';
const { id } = $page.params;
const dispatch = createEventDispatcher();
async function saveStorage(newStorage = false) {
try {
if (!storage.path) return errorNotification('Path is required.');
storage.path = storage.path.startsWith('/') ? storage.path : `/${storage.path}`;
storage.path = storage.path.endsWith('/') ? storage.path.slice(0, -1) : storage.path;
storage.path.replace(/\/\//g, '/');
await post(`/applications/${id}/storage.json`, {
path: storage.path,
storageId: storage.id,
newStorage
});
dispatch('refresh');
if (isNew) {
storage.path = null;
storage.id = null;
}
if (newStorage) toast.push('Storage saved.');
else toast.push('Storage updated.');
} catch ({ error }) {
return errorNotification(error);
}
}
async function removeStorage() {
try {
await del(`/applications/${id}/storage.json`, { path: storage.path });
dispatch('refresh');
toast.push('Storage deleted.');
} catch ({ error }) {
return errorNotification(error);
}
}
</script>
<td>
<input
bind:value={storage.path}
required
placeholder="eg: /sqlite.db"
class=" border border-dashed border-coolgray-300"
/>
</td>
<td>
{#if isNew}
<div class="flex items-center justify-center">
<button class="bg-green-600 hover:bg-green-500" on:click={() => saveStorage(true)}>Add</button
>
</div>
{:else}
<div class="flex flex-row justify-center space-x-2">
<div class="flex items-center justify-center">
<button class="" on:click={() => saveStorage(false)}>Set</button>
</div>
<div class="flex justify-center items-end">
<button class="bg-red-600 hover:bg-red-500" on:click={removeStorage}>Remove</button>
</div>
</div>
{/if}
</td>

View File

@@ -0,0 +1,63 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => {
const { status, body, teamId } = await getUserDetails(event, false);
if (status === 401) return { status, body };
const { id } = event.params;
try {
const persistentStorages = await db.getPersistentStorage(id);
return {
body: {
persistentStorages
}
};
} catch (error) {
return ErrorHandler(error);
}
};
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
const { path, newStorage, storageId } = await event.request.json();
try {
if (newStorage) {
await db.prisma.applicationPersistentStorage.create({
data: { path, application: { connect: { id } } }
});
} else {
await db.prisma.applicationPersistentStorage.update({
where: { id: storageId },
data: { path }
});
}
return {
status: 201
};
} catch (error) {
return ErrorHandler(error);
}
};
export const del: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
const { path } = await event.request.json();
try {
await db.prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: id, path } });
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
}
};

View File

@@ -0,0 +1,73 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params, stuff }) => {
let endpoint = `/applications/${params.id}/storage.json`;
const res = await fetch(endpoint);
if (res.ok) {
return {
props: {
application: stuff.application,
...(await res.json())
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${endpoint}`)
};
};
</script>
<script lang="ts">
export let application;
export let persistentStorages;
import { getDomain } from '$lib/components/common';
import { page } from '$app/stores';
import Storage from './_Storage.svelte';
import { get } from '$lib/api';
import Explainer from '$lib/components/Explainer.svelte';
const { id } = $page.params;
async function refreshStorage() {
const data = await get(`/applications/${id}/storage.json`);
persistentStorages = [...data.persistentStorages];
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">
Persistent storage for <a href={application.fqdn} target="_blank"
>{getDomain(application.fqdn)}</a
>
</div>
</div>
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4">
<div class="flex justify-center py-4 text-center">
<Explainer
customClass="w-full"
text={'You can specify any folder that you want to be persistent across deployments. <br>This is useful for storing data such as a database (SQLite) or a cache.'}
/>
</div>
<table class="mx-auto border-separate text-left">
<thead>
<tr class="h-12">
<th scope="col">Path</th>
</tr>
</thead>
<tbody>
{#each persistentStorages as storage}
{#key storage.id}
<tr>
<Storage on:refresh={refreshStorage} {storage} />
</tr>
{/key}
{/each}
<tr>
<Storage on:refresh={refreshStorage} isNew />
</tr>
</tbody>
</table>
</div>

View File

@@ -1,34 +1,19 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch }) => {
const endpoint = '/applications.json';
const res = await fetch(endpoint);
if (res.ok) {
return {
props: {
...(await res.json())
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${endpoint}`)
};
};
</script>
<script lang="ts"> <script lang="ts">
export let applications: Array<Application>; export let applications: Array<Application>;
import { session } from '$app/stores'; import { session } from '$app/stores';
import Application from './_Application.svelte'; import Application from './_Application.svelte';
import { post } from '$lib/api';
import { goto } from '$app/navigation';
async function newApplication() {
const { id } = await post('/applications/new', {});
return await goto(`/applications/${id}`, { replaceState: true });
}
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl ">Applications</div> <div class="mr-4 text-2xl ">Applications</div>
{#if $session.isAdmin} {#if $session.isAdmin}
<a href="/new/application" class="add-icon bg-green-600 hover:bg-green-500"> <div on:click={newApplication} class="add-icon cursor-pointer bg-green-600 hover:bg-green-500">
<svg <svg
class="w-6" class="w-6"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -42,7 +27,7 @@
d="M12 6v6m0 0v6m0-6h6m-6 0H6" d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg /></svg
> >
</a> </div>
{/if} {/if}
</div> </div>
<div class="flex flex-wrap justify-center"> <div class="flex flex-wrap justify-center">

View File

@@ -6,10 +6,7 @@ import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const name = uniqueName();
const { name } = await event.request.json();
if (!name) return { status: 400, body: { error: 'Missing name.' } };
try { try {
const { id } = await db.newApplication({ name, teamId }); const { id } = await db.newApplication({ name, teamId });
return { status: 201, body: { id } }; return { status: 201, body: { id } };

View File

@@ -8,12 +8,22 @@ export const get: RequestHandler = async (event) => {
if (status === 401) return { status, body }; if (status === 401) return { status, body };
try { try {
const applicationsCount = await (await db.listApplications(teamId)).length; const applicationsCount = await db.prisma.application.count({
const sourcesCount = await (await db.listSources(teamId)).length; where: { teams: { some: { id: teamId } } }
const destinationsCount = await (await db.listDestinations(teamId)).length; });
const teamsCount = await (await db.getMyTeams({ userId })).length; const sourcesCount = await db.prisma.gitSource.count({
const databasesCount = await (await db.listDatabases(teamId)).length; where: { teams: { some: { id: teamId } } }
const servicesCount = await (await db.listServices(teamId)).length; });
const destinationsCount = await db.prisma.destinationDocker.count({
where: { teams: { some: { id: teamId } } }
});
const teamsCount = await db.prisma.permission.count({ where: { userId } });
const databasesCount = await db.prisma.database.count({
where: { teams: { some: { id: teamId } } }
});
const servicesCount = await db.prisma.service.count({
where: { teams: { some: { id: teamId } } }
});
return { return {
body: { body: {
applicationsCount, applicationsCount,

View File

@@ -1,24 +1,3 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch }) => {
const url = `/databases.json`;
const res = await fetch(url);
if (res.ok) {
return {
props: {
...(await res.json())
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${url}`)
};
};
</script>
<script lang="ts"> <script lang="ts">
export let databases; export let databases;
import Clickhouse from '$lib/components/svg/databases/Clickhouse.svelte'; import Clickhouse from '$lib/components/svg/databases/Clickhouse.svelte';
@@ -27,11 +6,18 @@
import MySQL from '$lib/components/svg/databases/MySQL.svelte'; import MySQL from '$lib/components/svg/databases/MySQL.svelte';
import PostgreSQL from '$lib/components/svg/databases/PostgreSQL.svelte'; import PostgreSQL from '$lib/components/svg/databases/PostgreSQL.svelte';
import Redis from '$lib/components/svg/databases/Redis.svelte'; import Redis from '$lib/components/svg/databases/Redis.svelte';
import { post } from '$lib/api';
import { goto } from '$app/navigation';
async function newDatabase() {
const { id } = await post('/databases/new', {});
return await goto(`/databases/${id}`, { replaceState: true });
}
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Databases</div> <div class="mr-4 text-2xl tracking-tight">Databases</div>
<a href="/new/database" class="add-icon bg-purple-600 hover:bg-purple-500"> <div on:click={newDatabase} class="add-icon cursor-pointer bg-purple-600 hover:bg-purple-500">
<svg <svg
class="w-6" class="w-6"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -45,7 +31,7 @@
d="M12 6v6m0 0v6m0-6h6m-6 0H6" d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg /></svg
> >
</a> </div>
</div> </div>
<div class="flex flex-wrap justify-center"> <div class="flex flex-wrap justify-center">

View File

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

View File

@@ -1,4 +1,4 @@
import { getUserDetails } from '$lib/common'; import { getUserDetails, uniqueName } from '$lib/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database'; import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
@@ -6,9 +6,7 @@ import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const name = uniqueName();
const { name } = await event.request.json();
try { try {
const { id } = await db.newDatabase({ name, teamId }); const { id } = await db.newDatabase({ name, teamId });
return { status: 201, body: { id } }; return { status: 201, body: { id } };

View File

@@ -103,7 +103,7 @@
} }
async function forceRestartProxy() { async function forceRestartProxy() {
const sure = confirm( const sure = confirm(
'Are you sure you want to restart the proxy? Everyting will be reconfigured in ~10 sec.' 'Are you sure you want to restart the proxy? Everything will be reconfigured in ~10 secs.'
); );
if (sure) { if (sure) {
try { try {

View File

@@ -106,7 +106,7 @@
} }
async function forceRestartProxy() { async function forceRestartProxy() {
const sure = confirm( const sure = confirm(
'Are you sure you want to restart the proxy? Everyting will be reconfigured in ~10 sec.' 'Are you sure you want to restart the proxy? Everything will be reconfigured in ~10 secs.'
); );
if (sure) { if (sure) {
try { try {

View File

@@ -1,53 +0,0 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch }) => {
const url = `/common/getUniqueName.json`;
const res = await fetch(url);
if (res.ok) {
return {
props: {
...(await res.json())
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${url}`)
};
};
</script>
<script lang="ts">
export let name;
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { post } from '$lib/api';
import { errorNotification } from '$lib/form';
let nameEl: HTMLInputElement;
onMount(() => {
nameEl.focus();
});
async function handleSubmit() {
try {
const { id } = await post('/new/application.json', { name });
return await goto(`/applications/${id}`);
} catch ({ error }) {
return errorNotification(error);
}
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Add New Application</div>
</div>
<div class="pt-10">
<form on:submit|preventDefault={handleSubmit}>
<div class="flex flex-col items-center space-y-4">
<input name="name" placeholder="Application name" bind:this={nameEl} bind:value={name} />
<button type="submit" class="bg-green-600 hover:bg-green-500">Save</button>
</div>
</form>
</div>

View File

@@ -1,59 +0,0 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, session }) => {
const url = `/common/getUniqueName.json`;
const res = await fetch(url);
if (res.ok) {
return {
props: {
...(await res.json())
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${url}`)
};
};
</script>
<script lang="ts">
export let name;
import { errorNotification } from '$lib/form';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { post } from '$lib/api';
let autofocus;
onMount(() => {
autofocus.focus();
});
async function handleSubmit() {
try {
const { id } = await post('/new/database.json', { name });
return await goto(`/databases/${id}`);
} catch ({ error }) {
return errorNotification(error);
}
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Add New Database</div>
</div>
<div class="pt-10">
<form on:submit|preventDefault={handleSubmit}>
<div class="flex flex-col items-center space-y-4">
<input
name="name"
placeholder="Database name"
required
bind:this={autofocus}
bind:value={name}
/>
<button type="submit" class="bg-purple-600 hover:bg-purple-500">Save</button>
</div>
</form>
</div>

View File

@@ -1,59 +0,0 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, session }) => {
const url = `/common/getUniqueName.json`;
const res = await fetch(url);
if (res.ok) {
return {
props: {
...(await res.json())
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${url}`)
};
};
</script>
<script lang="ts">
export let name;
import { enhance, errorNotification } from '$lib/form';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { post } from '$lib/api';
let autofocus;
onMount(() => {
autofocus.focus();
});
async function handleSubmit() {
try {
const { id } = await post(`/new/service.json`, { name });
return await goto(`/services/${id}`);
} catch ({ error }) {
return errorNotification(error);
}
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Add New Service</div>
</div>
<div class="pt-10">
<form on:submit|preventDefault={handleSubmit}>
<div class="flex flex-col items-center space-y-4">
<input
name="name"
placeholder="Service name"
required
bind:this={autofocus}
bind:value={name}
/>
<button type="submit" class="bg-pink-600 hover:bg-pink-500">Save</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,90 @@
<script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
export let readOnly;
export let service;
</script>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">Ghost</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="email">Default Email Address</label>
<input
name="email"
id="email"
disabled
readonly
placeholder="Email address"
value={service.ghost.defaultEmail}
required
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="defaultPassword">Default Password</label>
<CopyPasswordField
id="defaultPassword"
isPasswordField
readonly
disabled
name="defaultPassword"
value={service.ghost.defaultPassword}
/>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">MariaDB</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mariadbUser">Username</label>
<CopyPasswordField
name="mariadbUser"
id="mariadbUser"
value={service.ghost.mariadbUser}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mariadbPassword">Password</label>
<CopyPasswordField
id="mariadbPassword"
isPasswordField
readonly
disabled
name="mariadbPassword"
value={service.ghost.mariadbPassword}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mariadbDatabase">Database</label>
<input
name="mariadbDatabase"
id="mariadbDatabase"
required
readonly={readOnly}
disabled={readOnly}
bind:value={service.ghost.mariadbDatabase}
placeholder="eg: ghost_db"
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mariadbRootUser">Root DB User</label>
<CopyPasswordField
id="mariadbRootUser"
isPasswordField
readonly
disabled
name="mariadbRootUser"
value={service.ghost.mariadbRootUser}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mariadbRootUserPassword">Root DB Password</label>
<CopyPasswordField
id="mariadbRootUserPassword"
isPasswordField
readonly
disabled
name="mariadbRootUserPassword"
value={service.ghost.mariadbRootUserPassword}
/>
</div>

View File

@@ -10,6 +10,7 @@
import Setting from '$lib/components/Setting.svelte'; import Setting from '$lib/components/Setting.svelte';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast'; import { toast } from '@zerodevx/svelte-toast';
import Ghost from './_Ghost.svelte';
import MinIo from './_MinIO.svelte'; import MinIo from './_MinIO.svelte';
import PlausibleAnalytics from './_PlausibleAnalytics.svelte'; import PlausibleAnalytics from './_PlausibleAnalytics.svelte';
import VsCodeServer from './_VSCodeServer.svelte'; import VsCodeServer from './_VSCodeServer.svelte';
@@ -142,6 +143,8 @@
<VsCodeServer {service} /> <VsCodeServer {service} />
{:else if service.type === 'wordpress'} {:else if service.type === 'wordpress'}
<Wordpress bind:service {isRunning} {readOnly} /> <Wordpress bind:service {isRunning} {readOnly} />
{:else if service.type === 'ghost'}
<Ghost bind:service {readOnly} />
{/if} {/if}
</div> </div>
</form> </form>

View File

@@ -35,6 +35,7 @@
} }
if (service.plausibleAnalytics?.email && service.plausibleAnalytics.username) readOnly = true; if (service.plausibleAnalytics?.email && service.plausibleAnalytics.username) readOnly = true;
if (service.wordpress?.mysqlDatabase) readOnly = true; if (service.wordpress?.mysqlDatabase) readOnly = true;
if (service.ghost?.mariadbDatabase && service.ghost.mariadbDatabase) readOnly = true;
return { return {
props: { props: {

View File

@@ -38,6 +38,9 @@
import { post } from '$lib/api'; import { post } from '$lib/api';
import VaultWarden from '$lib/components/svg/services/VaultWarden.svelte'; import VaultWarden from '$lib/components/svg/services/VaultWarden.svelte';
import LanguageTool from '$lib/components/svg/services/LanguageTool.svelte'; import LanguageTool from '$lib/components/svg/services/LanguageTool.svelte';
import N8n from '$lib/components/svg/services/N8n.svelte';
import UptimeKuma from '$lib/components/svg/services/UptimeKuma.svelte';
import Ghost from '$lib/components/svg/services/Ghost.svelte';
const { id } = $page.params; const { id } = $page.params;
const from = $page.url.searchParams.get('from'); const from = $page.url.searchParams.get('from');
@@ -77,6 +80,12 @@
<VaultWarden isAbsolute /> <VaultWarden isAbsolute />
{:else if type.name === 'languagetool'} {:else if type.name === 'languagetool'}
<LanguageTool isAbsolute /> <LanguageTool isAbsolute />
{:else if type.name === 'n8n'}
<N8n isAbsolute />
{:else if type.name === 'uptimekuma'}
<UptimeKuma isAbsolute />
{:else if type.name === 'ghost'}
<Ghost isAbsolute />
{/if}{type.fancyName} {/if}{type.fancyName}
</button> </button>
</form> </form>

View File

@@ -0,0 +1,23 @@
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,
ghost: { mariadbDatabase }
} = await event.request.json();
if (fqdn) fqdn = fqdn.toLowerCase();
try {
await db.updateGhostService({ id, fqdn, name, mariadbDatabase });
return { status: 201 };
} catch (error) {
return ErrorHandler(error);
}
};

View File

@@ -0,0 +1,133 @@
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';
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 {
type,
version,
destinationDockerId,
destinationDocker,
serviceSecret,
fqdn,
ghost: {
defaultEmail,
defaultPassword,
mariadbRootUser,
mariadbRootUserPassword,
mariadbDatabase,
mariadbPassword,
mariadbUser
}
} = 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 = {
ghost: {
image: `${image}:${version}`,
volume: `${id}-ghost:/bitnami/ghost`,
environmentVariables: {
GHOST_HOST: domain,
GHOST_EMAIL: defaultEmail,
GHOST_PASSWORD: defaultPassword,
GHOST_DATABASE_HOST: `${id}-mariadb`,
GHOST_DATABASE_USER: mariadbUser,
GHOST_DATABASE_PASSWORD: mariadbPassword,
GHOST_DATABASE_NAME: mariadbDatabase,
GHOST_DATABASE_PORT_NUMBER: 3306
}
},
mariadb: {
image: `bitnami/mariadb:latest`,
volume: `${id}-mariadb:/bitnami/mariadb`,
environmentVariables: {
MARIADB_USER: mariadbUser,
MARIADB_PASSWORD: mariadbPassword,
MARIADB_DATABASE: mariadbDatabase,
MARIADB_ROOT_USER: mariadbRootUser,
MARIADB_ROOT_PASSWORD: mariadbRootUserPassword
}
}
};
if (serviceSecret.length > 0) {
serviceSecret.forEach((secret) => {
config.ghost.environmentVariables[secret.name] = secret.value;
});
}
const composeFile = {
version: '3.8',
services: {
[id]: {
container_name: id,
image: config.ghost.image,
networks: [network],
volumes: [config.ghost.volume],
environment: config.ghost.environmentVariables,
restart: 'always',
labels: makeLabelForServices('ghost'),
depends_on: [`${id}-mariadb`]
},
[`${id}-mariadb`]: {
container_name: `${id}-mariadb`,
image: config.mariadb.image,
networks: [network],
volumes: [config.mariadb.volume],
environment: config.mariadb.environmentVariables,
restart: 'always'
}
},
networks: {
[network]: {
external: true
}
},
volumes: {
[config.ghost.volume.split(':')[0]]: {
name: config.ghost.volume.split(':')[0]
},
[config.mariadb.volume.split(':')[0]]: {
name: config.mariadb.volume.split(':')[0]
}
}
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try {
if (version === 'latest') {
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) {
return ErrorHandler(error);
}
} catch (error) {
return ErrorHandler(error);
}
};

View File

@@ -0,0 +1,39 @@
import { getUserDetails, removeDestinationDocker } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import { checkContainer } 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, fqdn } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
let found = await checkContainer(engine, id);
if (found) {
await removeDestinationDocker({ id, engine });
}
found = await checkContainer(engine, `${id}-mariadb`);
if (found) {
await removeDestinationDocker({ id: `${id}-mariadb`, engine });
}
} catch (error) {
console.error(error);
}
}
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
}
};

View File

@@ -4,7 +4,8 @@ import {
generateDatabaseConfiguration, generateDatabaseConfiguration,
getServiceImage, getServiceImage,
getVersions, getVersions,
ErrorHandler ErrorHandler,
getServiceImages
} from '$lib/database'; } from '$lib/database';
import { dockerInstance } from '$lib/docker'; import { dockerInstance } from '$lib/docker';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
@@ -23,7 +24,13 @@ export const get: RequestHandler = async (event) => {
const host = getEngine(destinationDocker.engine); const host = getEngine(destinationDocker.engine);
const docker = dockerInstance({ destinationDocker }); const docker = dockerInstance({ destinationDocker });
const baseImage = getServiceImage(type); const baseImage = getServiceImage(type);
const images = getServiceImages(type);
docker.engine.pull(`${baseImage}:${version}`); docker.engine.pull(`${baseImage}:${version}`);
if (images?.length > 0) {
for (const image of images) {
docker.engine.pull(`${image}:latest`);
}
}
try { try {
const { stdout } = await asyncExecShell( const { stdout } = await asyncExecShell(
`DOCKER_HOST=${host} docker inspect --format '{{json .State}}' ${id}` `DOCKER_HOST=${host} docker inspect --format '{{json .State}}' ${id}`

View File

@@ -39,6 +39,9 @@
import cuid from 'cuid'; import cuid from 'cuid';
import { browser } from '$app/env'; import { browser } from '$app/env';
import LanguageTool from '$lib/components/svg/services/LanguageTool.svelte'; import LanguageTool from '$lib/components/svg/services/LanguageTool.svelte';
import N8n from '$lib/components/svg/services/N8n.svelte';
import UptimeKuma from '$lib/components/svg/services/UptimeKuma.svelte';
import Ghost from '$lib/components/svg/services/Ghost.svelte';
export let service; export let service;
export let isRunning; export let isRunning;
@@ -109,6 +112,18 @@
<a href="https://languagetool.org/dev" target="_blank"> <a href="https://languagetool.org/dev" target="_blank">
<LanguageTool /> <LanguageTool />
</a> </a>
{:else if service.type === 'n8n'}
<a href="https://n8n.io" target="_blank">
<N8n />
</a>
{:else if service.type === 'uptimekuma'}
<a href="https://github.com/louislam/uptime-kuma" target="_blank">
<UptimeKuma />
</a>
{:else if service.type === 'ghost'}
<a href="https://ghost.org" target="_blank">
<Ghost />
</a>
{/if} {/if}
</div> </div>
</div> </div>

View File

@@ -41,7 +41,7 @@ export const post: RequestHandler = async (event) => {
networks: [network], networks: [network],
environment: config.environmentVariables, environment: config.environmentVariables,
restart: 'always', restart: 'always',
volumes: [`${id}-ngrams:/ngrams`], volumes: [config.volume],
labels: makeLabelForServices('languagetool') labels: makeLabelForServices('languagetool')
} }
}, },
@@ -51,20 +51,20 @@ export const post: RequestHandler = async (event) => {
} }
}, },
volumes: { volumes: {
[`${id}-ngrams`]: { [config.volume.split(':')[0]]: {
external: true name: config.volume.split(':')[0]
} }
} }
}; };
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try {
await asyncExecShell(`DOCKER_HOST=${host} docker volume create ${id}-ngrams`);
} catch (error) {
console.log(error);
}
try { try {
if (version === 'latest') {
await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`
);
}
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return { return {
status: 200 status: 200

View File

@@ -13,7 +13,7 @@ export const post: RequestHandler = async (event) => {
if (fqdn) fqdn = fqdn.toLowerCase(); if (fqdn) fqdn = fqdn.toLowerCase();
try { try {
await db.updateNocoDbOrMinioService({ id, fqdn, name }); await db.updateService({ id, fqdn, name });
return { status: 201 }; return { status: 201 };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);

View File

@@ -76,19 +76,13 @@ export const post: RequestHandler = async (event) => {
}, },
volumes: { volumes: {
[config.volume.split(':')[0]]: { [config.volume.split(':')[0]]: {
external: true name: config.volume.split(':')[0]
} }
} }
}; };
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try {
await asyncExecShell(
`DOCKER_HOST=${host} docker volume create ${config.volume.split(':')[0]}`
);
} catch (error) {
console.log(error);
}
try { try {
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
await db.updateMinioService({ id, publicPort }); await db.updateMinioService({ id, publicPort });

View File

@@ -4,25 +4,16 @@ import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { id } = event.params;
let { name, fqdn, port, buildCommand, startCommand, installCommand } = await event.request.json(); let { name, fqdn } = await event.request.json();
if (fqdn) fqdn = fqdn.toLowerCase(); if (fqdn) fqdn = fqdn.toLowerCase();
if (port) port = Number(port);
try { try {
const { id } = await db.importApplication({ await db.updateService({ id, fqdn, name });
name, return { status: 201 };
teamId,
fqdn,
port,
buildCommand,
startCommand,
installCommand
});
return { status: 201, body: { id } };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);
} }

View File

@@ -0,0 +1,77 @@
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';
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 { type, version, destinationDockerId, destinationDocker, serviceSecret } = 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 = {
image: `${image}:${version}`,
volume: `${id}-n8n:/root/.n8n`,
environmentVariables: {}
};
if (serviceSecret.length > 0) {
serviceSecret.forEach((secret) => {
config.environmentVariables[secret.name] = secret.value;
});
}
const composeFile = {
version: '3.8',
services: {
[id]: {
container_name: id,
image: config.image,
networks: [network],
volumes: [config.volume],
environment: config.environmentVariables,
restart: 'always',
labels: makeLabelForServices('n8n')
}
},
networks: {
[network]: {
external: true
}
},
volumes: {
[config.volume.split(':')[0]]: {
name: config.volume.split(':')[0]
}
}
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try {
if (version === 'latest') {
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) {
return ErrorHandler(error);
}
} catch (error) {
return ErrorHandler(error);
}
};

View File

@@ -0,0 +1,35 @@
import { getUserDetails, removeDestinationDocker } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import { checkContainer } 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, fqdn } = 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);
}
}
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
}
};

View File

@@ -12,7 +12,7 @@ export const post: RequestHandler = async (event) => {
if (fqdn) fqdn = fqdn.toLowerCase(); if (fqdn) fqdn = fqdn.toLowerCase();
try { try {
await db.updateNocoDbOrMinioService({ id, fqdn, name }); await db.updateService({ id, fqdn, name });
return { status: 201 }; return { status: 201 };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);

View File

@@ -52,6 +52,11 @@ export const post: RequestHandler = async (event) => {
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try { try {
if (version === 'latest') {
await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`
);
}
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return { return {
status: 200 status: 200

View File

@@ -158,29 +158,21 @@ COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`;
}, },
volumes: { volumes: {
[config.postgresql.volume.split(':')[0]]: { [config.postgresql.volume.split(':')[0]]: {
external: true name: config.postgresql.volume.split(':')[0]
}, },
[config.clickhouse.volume.split(':')[0]]: { [config.clickhouse.volume.split(':')[0]]: {
external: true name: config.clickhouse.volume.split(':')[0]
} }
} }
}; };
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try { if (version === 'latest') {
await asyncExecShell( await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
`DOCKER_HOST=${host} docker volume create ${config.postgresql.volume.split(':')[0]}`
);
await asyncExecShell(
`DOCKER_HOST=${host} docker volume create ${config.clickhouse.volume.split(':')[0]}`
);
} catch (error) {
console.log(error);
} }
await asyncExecShell( await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up --build -d` `DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up --build -d`
); );
return { return {
status: 200 status: 200
}; };

View File

@@ -0,0 +1,20 @@
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,77 @@
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';
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 { type, version, destinationDockerId, destinationDocker, serviceSecret } = 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 = {
image: `${image}:${version}`,
volume: `${id}-uptimekuma:/app/data`,
environmentVariables: {}
};
if (serviceSecret.length > 0) {
serviceSecret.forEach((secret) => {
config.environmentVariables[secret.name] = secret.value;
});
}
const composeFile = {
version: '3.8',
services: {
[id]: {
container_name: id,
image: config.image,
networks: [network],
volumes: [config.volume],
environment: config.environmentVariables,
restart: 'always',
labels: makeLabelForServices('uptimekuma')
}
},
networks: {
[network]: {
external: true
}
},
volumes: {
[config.volume.split(':')[0]]: {
name: config.volume.split(':')[0]
}
}
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try {
if (version === 'latest') {
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) {
return ErrorHandler(error);
}
} catch (error) {
return ErrorHandler(error);
}
};

View File

@@ -0,0 +1,35 @@
import { getUserDetails, removeDestinationDocker } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import { checkContainer } 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, fqdn } = 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);
}
}
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
}
};

View File

@@ -52,20 +52,18 @@ export const post: RequestHandler = async (event) => {
}, },
volumes: { volumes: {
[config.volume.split(':')[0]]: { [config.volume.split(':')[0]]: {
external: true name: config.volume.split(':')[0]
} }
} }
}; };
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try { try {
await asyncExecShell( if (version === 'latest') {
`DOCKER_HOST=${host} docker volume create ${config.volume.split(':')[0]}` await asyncExecShell(
); `DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`
} catch (error) { );
console.log(error); }
}
try {
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return { return {
status: 200 status: 200

View File

@@ -61,29 +61,20 @@ export const post: RequestHandler = async (event) => {
}, },
volumes: { volumes: {
[config.volume.split(':')[0]]: { [config.volume.split(':')[0]]: {
external: true name: config.volume.split(':')[0]
} }
} }
}; };
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try { if (version === 'latest') {
await asyncExecShell( await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
`DOCKER_HOST=${host} docker volume create ${config.volume.split(':')[0]}`
);
} catch (error) {
console.log(error);
}
try {
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
} }
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return {
status: 200
};
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);
} }

View File

@@ -72,6 +72,7 @@ export const post: RequestHandler = async (event) => {
container_name: id, container_name: id,
image: config.wordpress.image, image: config.wordpress.image,
environment: config.wordpress.environmentVariables, environment: config.wordpress.environmentVariables,
volumes: [config.wordpress.volume],
networks: [network], networks: [network],
restart: 'always', restart: 'always',
depends_on: [`${id}-mysql`], depends_on: [`${id}-mysql`],
@@ -80,6 +81,7 @@ export const post: RequestHandler = async (event) => {
[`${id}-mysql`]: { [`${id}-mysql`]: {
container_name: `${id}-mysql`, container_name: `${id}-mysql`,
image: config.mysql.image, image: config.mysql.image,
volumes: [config.mysql.volume],
environment: config.mysql.environmentVariables, environment: config.mysql.environmentVariables,
networks: [network], networks: [network],
restart: 'always' restart: 'always'
@@ -91,29 +93,22 @@ export const post: RequestHandler = async (event) => {
} }
}, },
volumes: { volumes: {
[config.mysql.volume.split(':')[0]]: {
external: true
},
[config.wordpress.volume.split(':')[0]]: { [config.wordpress.volume.split(':')[0]]: {
external: true name: config.wordpress.volume.split(':')[0]
},
[config.mysql.volume.split(':')[0]]: {
name: config.mysql.volume.split(':')[0]
} }
} }
}; };
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try {
await asyncExecShell(
`DOCKER_HOST=${host} docker volume create ${config.mysql.volume.split(':')[0]}`
);
await asyncExecShell(
`DOCKER_HOST=${host} docker volume create ${config.wordpress.volume.split(':')[0]}`
);
} catch (error) {
console.log(error);
}
try { try {
if (version === 'latest') {
await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`
);
}
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return { return {
status: 200 status: 200

View File

@@ -1,24 +1,3 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch }) => {
const url = `/services.json`;
const res = await fetch(url);
if (res.ok) {
return {
props: {
...(await res.json())
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${url}`)
};
};
</script>
<script lang="ts"> <script lang="ts">
import PlausibleAnalytics from '$lib/components/svg/services/PlausibleAnalytics.svelte'; import PlausibleAnalytics from '$lib/components/svg/services/PlausibleAnalytics.svelte';
import NocoDb from '$lib/components/svg/services/NocoDB.svelte'; import NocoDb from '$lib/components/svg/services/NocoDB.svelte';
@@ -27,13 +6,22 @@
import Wordpress from '$lib/components/svg/services/Wordpress.svelte'; import Wordpress from '$lib/components/svg/services/Wordpress.svelte';
import VaultWarden from '$lib/components/svg/services/VaultWarden.svelte'; import VaultWarden from '$lib/components/svg/services/VaultWarden.svelte';
import LanguageTool from '$lib/components/svg/services/LanguageTool.svelte'; import LanguageTool from '$lib/components/svg/services/LanguageTool.svelte';
import { post } from '$lib/api';
import { goto } from '$app/navigation';
import N8n from '$lib/components/svg/services/N8n.svelte';
import UptimeKuma from '$lib/components/svg/services/UptimeKuma.svelte';
import Ghost from '$lib/components/svg/services/Ghost.svelte';
export let services; export let services;
async function newService() {
const { id } = await post('/services/new', {});
return await goto(`/services/${id}`, { replaceState: true });
}
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Services</div> <div class="mr-4 text-2xl tracking-tight">Services</div>
<a href="/new/service" class="add-icon bg-pink-600 hover:bg-pink-500"> <div on:click={newService} class="add-icon cursor-pointer bg-pink-600 hover:bg-pink-500">
<svg <svg
class="w-6" class="w-6"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -47,7 +35,7 @@
d="M12 6v6m0 0v6m0-6h6m-6 0H6" d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg /></svg
> >
</a> </div>
</div> </div>
<div class="flex flex-wrap justify-center"> <div class="flex flex-wrap justify-center">
@@ -73,6 +61,12 @@
<VaultWarden isAbsolute /> <VaultWarden isAbsolute />
{:else if service.type === 'languagetool'} {:else if service.type === 'languagetool'}
<LanguageTool isAbsolute /> <LanguageTool isAbsolute />
{:else if service.type === 'n8n'}
<N8n isAbsolute />
{:else if service.type === 'uptimekuma'}
<UptimeKuma isAbsolute />
{:else if service.type === 'ghost'}
<Ghost isAbsolute />
{/if} {/if}
<div class="font-bold text-xl text-center truncate"> <div class="font-bold text-xl text-center truncate">
{service.name} {service.name}

View File

@@ -6,9 +6,7 @@ import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const name = uniqueName();
const { name } = await event.request.json();
try { try {
const { id } = await db.newService({ name, teamId }); const { id } = await db.newService({ name, teamId });
return { status: 201, body: { id } }; return { status: 201, body: { id } };

View File

@@ -91,23 +91,21 @@
</div> </div>
</div> </div>
{/if} {/if}
<div class="mx-auto max-w-2xl"> <div class="flex flex-wrap justify-center">
<div class="flex flex-wrap justify-center"> {#each teams as team}
{#each teams as team} <a href="/teams/{team.teamId}" class="w-96 p-2 no-underline">
<a href="/teams/{team.teamId}" class="w-96 p-2 no-underline"> <div
<div class="box-selection relative"
class="box-selection relative" class:hover:bg-cyan-600={team.team?.id !== '0'}
class:hover:bg-cyan-600={team.team?.id !== '0'} class:hover:bg-red-500={team.team?.id === '0'}
class:hover:bg-red-500={team.team?.id === '0'} >
> <div class="truncate text-center text-xl font-bold">
<div class="truncate text-center text-xl font-bold">{team.team.name}</div> {team.team.name}
<div class="text-center text-xs"> {team.team?.id === '0' ? '(root)' : ''}
({team.team?.id === '0' ? 'root team - ' : ''}{team.permission})
</div>
<div class="mt-1 text-center">{team.team._count.users} member(s)</div>
</div> </div>
</a>
{/each} <div class="mt-1 text-center">{team.team._count.users} member(s)</div>
</div> </div>
</a>
{/each}
</div> </div>

View File

@@ -9,7 +9,7 @@ import { dev } from '$app/env';
export const options: RequestHandler = async () => { export const options: RequestHandler = async () => {
return { return {
status: 200, status: 204,
headers: { headers: {
'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type, Authorization', 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
@@ -47,7 +47,7 @@ export const post: RequestHandler = async (event) => {
const applicationFound = await db.getApplicationWebhook({ projectId, branch }); const applicationFound = await db.getApplicationWebhook({ projectId, branch });
if (applicationFound) { if (applicationFound) {
const webhookSecret = applicationFound.gitSource.githubApp.webhookSecret; const webhookSecret = applicationFound.gitSource.githubApp.webhookSecret || null;
const hmac = crypto.createHmac('sha256', webhookSecret); const hmac = crypto.createHmac('sha256', webhookSecret);
const digest = Buffer.from( const digest = Buffer.from(
'sha256=' + hmac.update(JSON.stringify(body)).digest('hex'), 'sha256=' + hmac.update(JSON.stringify(body)).digest('hex'),
@@ -88,6 +88,18 @@ export const post: RequestHandler = async (event) => {
where: { id: applicationFound.id }, where: { id: applicationFound.id },
data: { updatedAt: new Date() } data: { updatedAt: new Date() }
}); });
await db.prisma.build.create({
data: {
id: buildId,
applicationId: applicationFound.id,
destinationDockerId: applicationFound.destinationDocker.id,
gitSourceId: applicationFound.gitSource.id,
githubAppId: applicationFound.gitSource.githubApp?.id,
gitlabAppId: applicationFound.gitSource.gitlabApp?.id,
status: 'queued',
type: 'webhook_commit'
}
});
await buildQueue.add(buildId, { await buildQueue.add(buildId, {
build_id: buildId, build_id: buildId,
type: 'webhook_commit', type: 'webhook_commit',
@@ -136,6 +148,18 @@ export const post: RequestHandler = async (event) => {
where: { id: applicationFound.id }, where: { id: applicationFound.id },
data: { updatedAt: new Date() } data: { updatedAt: new Date() }
}); });
await db.prisma.build.create({
data: {
id: buildId,
applicationId: applicationFound.id,
destinationDockerId: applicationFound.destinationDocker.id,
gitSourceId: applicationFound.gitSource.id,
githubAppId: applicationFound.gitSource.githubApp?.id,
gitlabAppId: applicationFound.gitSource.gitlabApp?.id,
status: 'queued',
type: 'webhook_pr'
}
});
await buildQueue.add(buildId, { await buildQueue.add(buildId, {
build_id: buildId, build_id: buildId,
type: 'webhook_pr', type: 'webhook_pr',

View File

@@ -9,7 +9,7 @@ import { dev } from '$app/env';
export const options: RequestHandler = async () => { export const options: RequestHandler = async () => {
return { return {
status: 200, status: 204,
headers: { headers: {
'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type, Authorization', 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
@@ -52,6 +52,18 @@ export const post: RequestHandler = async (event) => {
where: { id: applicationFound.id }, where: { id: applicationFound.id },
data: { updatedAt: new Date() } data: { updatedAt: new Date() }
}); });
await db.prisma.build.create({
data: {
id: buildId,
applicationId: applicationFound.id,
destinationDockerId: applicationFound.destinationDocker.id,
gitSourceId: applicationFound.gitSource.id,
githubAppId: applicationFound.gitSource.githubApp?.id,
gitlabAppId: applicationFound.gitSource.gitlabApp?.id,
status: 'queued',
type: 'webhook_commit'
}
});
await buildQueue.add(buildId, { await buildQueue.add(buildId, {
build_id: buildId, build_id: buildId,
type: 'webhook_commit', type: 'webhook_commit',
@@ -133,6 +145,18 @@ export const post: RequestHandler = async (event) => {
where: { id: applicationFound.id }, where: { id: applicationFound.id },
data: { updatedAt: new Date() } data: { updatedAt: new Date() }
}); });
await db.prisma.build.create({
data: {
id: buildId,
applicationId: applicationFound.id,
destinationDockerId: applicationFound.destinationDocker.id,
gitSourceId: applicationFound.gitSource.id,
githubAppId: applicationFound.gitSource.githubApp?.id,
gitlabAppId: applicationFound.gitSource.gitlabApp?.id,
status: 'queued',
type: 'webhook_mr'
}
});
await buildQueue.add(buildId, { await buildQueue.add(buildId, {
build_id: buildId, build_id: buildId,
type: 'webhook_mr', type: 'webhook_mr',

View File

@@ -7,7 +7,7 @@ import cookie from 'cookie';
export const options: RequestHandler = async () => { export const options: RequestHandler = async () => {
return { return {
status: 200, status: 204,
headers: { headers: {
'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type, Authorization', 'Access-Control-Allow-Headers': 'Content-Type, Authorization',

View File

@@ -2,23 +2,19 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/* poppins-regular - latin-ext_latin_devanagari */
@font-face { @font-face {
font-family: 'Poppins'; font-family: 'Poppins';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: local(''), url('/poppins-v19-latin-ext_latin_devanagari-regular.woff2') format('woff2'), src: local(''), url('/poppins-v19-latin-ext_latin_devanagari-regular.woff2') format('woff2'),
/* Chrome 26+, Opera 23+, Firefox 39+ */ url('/poppins-v19-latin-ext_latin_devanagari-regular.woff') format('woff');
url('/poppins-v19-latin-ext_latin_devanagari-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
} }
/* poppins-500 - latin-ext_latin_devanagari */
@font-face { @font-face {
font-family: 'Poppins'; font-family: 'Poppins';
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
src: local(''), url('/poppins-v19-latin-ext_latin_devanagari-500.woff2') format('woff2'), src: local(''), url('/poppins-v19-latin-ext_latin_devanagari-500.woff2') format('woff2'),
/* Chrome 26+, Opera 23+, Firefox 39+ */ url('/poppins-v19-latin-ext_latin_devanagari-500.woff') url('/poppins-v19-latin-ext_latin_devanagari-500.woff') format('woff');
format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
} }
html { html {
@@ -41,8 +37,51 @@ textarea {
@apply min-w-[24rem] rounded border border-transparent bg-transparent bg-coolgray-200 p-2 text-xs tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:border disabled:border-dashed disabled:border-coolgray-300 disabled:bg-transparent md:text-sm; @apply min-w-[24rem] rounded border border-transparent bg-transparent bg-coolgray-200 p-2 text-xs tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:border disabled:border-dashed disabled:border-coolgray-300 disabled:bg-transparent md:text-sm;
} }
#svelte .custom-select-wrapper .selectContainer.disabled input {
@apply placeholder:text-stone-600;
}
#svelte .custom-select-wrapper .selectContainer input {
@apply text-white;
}
#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;
}
#svelte .listContainer {
@apply bg-coolgray-400 text-white scrollbar-w-2 scrollbar-thumb-coollabs scrollbar-track-coolgray-200;
}
#svelte .item.hover {
@apply bg-coolgray-100 text-white;
}
#svelte .item.active {
@apply bg-coollabs text-white;
}
select { select {
@apply h-12 w-96 rounded bg-coolgray-200 p-2 text-xs font-bold tracking-tight text-white 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);
--inputColor: white;
--multiItemPadding: 0;
--multiSelectPadding: 0 0.5rem 0 0.5rem;
--border: none;
--placeholderColor: rgb(87 83 78);
--listBackground: rgb(32 32 32);
--itemColor: white;
--itemHoverBG: rgb(107 22 237);
--multiItemBG: rgb(32 32 32);
--multiClearHoverBG: transparent;
--multiClearHoverFill: rgb(239 68 68);
--multiItemActiveBG: transparent;
--multiClearBG: transparent;
--clearSelectFocusColor: white;
--clearSelectHoverColor: rgb(239 68 68);
--multiItemBorderRadius: 0.25rem;
--listShadow: none;
} }
label { label {
@@ -69,7 +108,7 @@ a {
} }
.nav-side { .nav-side {
@apply relative right-0 top-0 z-50 m-5 flex flex-wrap items-center justify-end space-x-2 bg-coolblack/40 text-white sm:absolute; @apply absolute right-0 top-0 z-50 m-5 flex flex-wrap items-center justify-end space-x-2 bg-coolblack/40 text-white;
} }
.add-icon { .add-icon {

BIN
static/ghost.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -1,5 +1,5 @@
const defaultTheme = require('tailwindcss/defaultTheme'); const defaultTheme = require('tailwindcss/defaultTheme');
const colors = require('tailwindcss/colors'); // const colors = require('tailwindcss/colors');
module.exports = { module.exports = {
content: ['./**/*.html', './src/**/*.{js,jsx,ts,tsx,svelte}'], content: ['./**/*.html', './src/**/*.{js,jsx,ts,tsx,svelte}'],
important: true, important: true,
@@ -18,7 +18,6 @@ module.exports = {
sans: ['Poppins', ...defaultTheme.fontFamily.sans] sans: ['Poppins', ...defaultTheme.fontFamily.sans]
}, },
colors: { colors: {
...colors,
coollabs: '#6B16ED', coollabs: '#6B16ED',
'coollabs-100': '#7317FF', 'coollabs-100': '#7317FF',
coolblack: '#161616', coolblack: '#161616',

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