Compare commits

...

92 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
Andras Bacsai
5adbd5e784 Merge pull request #210 from coollabsio/v2.0.28
v2.0.28
2022-03-04 15:39:35 +01:00
Andras Bacsai
5b2afa79d7 chore: version++ 2022-03-04 15:20:03 +01:00
Andras Bacsai
dc4e6d02b7 feat: Service secrets 2022-03-04 15:14:25 +01:00
Andras Bacsai
8ae61c8f78 fix: do not error if proxy is not running 2022-03-04 14:20:20 +01:00
Andras Bacsai
684b8e0914 Merge pull request #207 from coollabsio/v2.0.27
v2.0.27
2022-03-02 21:15:32 +01:00
Andras Bacsai
7c3314abae force image deletion 2022-03-02 21:14:53 +01:00
Andras Bacsai
ab9f8ff356 Before latest image 2022-03-02 21:08:36 +01:00
Andras Bacsai
892d8cd5c1 Reload proxy after ssl renewal 2022-03-02 21:04:42 +01:00
Andras Bacsai
8b8b45778d fix: application state in UI 2022-03-02 21:01:25 +01:00
Andras Bacsai
6655fb182c fix: cleanup coolify images 2022-03-02 20:57:28 +01:00
Andras Bacsai
0926d40247 fix: Reload haproxy if new cert is added 2022-03-02 20:43:16 +01:00
Andras Bacsai
ddc4d36688 Fix cleanup process of old coolify images 2022-03-02 20:32:29 +01:00
Andras Bacsai
53e1f22eb1 fix: check when a container is running 2022-03-02 20:32:18 +01:00
Andras Bacsai
3d2a34737b prevent restarting container check 2022-03-02 15:52:22 +01:00
Andras Bacsai
ebde77008c Cleanup coolify image fix 2022-03-02 15:52:06 +01:00
Andras Bacsai
3d27fd04ba cleanup old images of coolify 2022-03-02 14:53:43 +01:00
Andras Bacsai
d9fcaf3473 update packages 2022-03-02 14:53:33 +01:00
Andras Bacsai
d266f761aa Stats open with auth 2022-03-02 14:38:51 +01:00
Andras Bacsai
1d01405412 chore: Version++ 2022-03-02 13:38:58 +01:00
Andras Bacsai
7c62eb5bd6 feat: Send version with update request 2022-03-02 13:38:45 +01:00
Andras Bacsai
4dcc76d366 fix: Update process 2022-03-02 13:37:06 +01:00
Andras Bacsai
d2fad19a11 Update package.json 2022-03-02 12:23:40 +01:00
Andras Bacsai
7c92c4c964 Merge pull request #206 from coollabsio/feat/languagetool
v2.0.26
2022-03-02 12:22:44 +01:00
Andras Bacsai
5a71d33236 chore: Version++ 2022-03-02 12:20:20 +01:00
Andras Bacsai
1b4db4f793 fix 2022-03-02 12:20:02 +01:00
Andras Bacsai
c084b22815 fix: volume name 2022-03-02 12:17:48 +01:00
Andras Bacsai
acacef95cd fix: reload proxy on ssl cert 2022-03-02 12:10:12 +01:00
Andras Bacsai
5d722183d3 feat: Languagetool service 2022-03-02 11:57:03 +01:00
118 changed files with 2981 additions and 1144 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.24", "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.69", "@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.283",
"@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.20", "@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.0", "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.6", "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.7", "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.5.0", "ts-node": "10.7.0",
"tslib": "2.3.1", "tslib": "2.3.1",
"typescript": "4.5.5" "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.17.9", "@sentry/node": "6.19.2",
"bcrypt": "5.0.1", "bcrypt": "5.0.1",
"bullmq": "1.74.2", "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.7", "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.1", "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"

563
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
-- CreateTable
CREATE TABLE "ServiceSecret" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"value" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"serviceId" TEXT NOT NULL,
CONSTRAINT "ServiceSecret_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "ServiceSecret_name_serviceId_key" ON "ServiceSecret"("name", "serviceId");

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
@@ -122,6 +136,18 @@ model Secret {
@@unique([name, applicationId, isPRMRSecret]) @@unique([name, applicationId, isPRMRSecret])
} }
model ServiceSecret {
id String @id @default(cuid())
name String
value String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
serviceId String
@@unique([name, serviceId])
}
model BuildLog { model BuildLog {
id String @id @default(cuid()) id String @id @default(cuid())
applicationId String? applicationId String?
@@ -252,6 +278,8 @@ model Service {
minio Minio? minio Minio?
vscodeserver Vscodeserver? vscodeserver Vscodeserver?
wordpress Wordpress? wordpress Wordpress?
ghost Ghost?
serviceSecret ServiceSecret[]
} }
model PlausibleAnalytics { model PlausibleAnalytics {
@@ -305,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

@@ -75,7 +75,7 @@
/> />
{/if} {/if}
<div class="absolute top-0 right-0 m-3 cursor-pointer text-warmGray-600 hover:text-white"> <div class="absolute top-0 right-0 m-3 cursor-pointer text-stone-600 hover:text-white">
<div class="flex space-x-2"> <div class="flex space-x-2">
{#if isPasswordField} {#if isPasswordField}
<div on:click={() => (showPassword = !showPassword)}> <div on:click={() => (showPassword = !showPassword)}>

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,27 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 mx-auto'}
fill="none"
viewBox="0 0 140 140"
data-lt-extension-installed="true"
><g clip-path="url(#clip0)"
><path
fill="#fff"
fill-rule="evenodd"
d="M140 43.602c0-1.662.001-3.324-.01-4.987-.008-1.4-.024-2.8-.062-4.2-.082-3.05-.262-6.126-.805-9.142-.55-3.06-1.448-5.907-2.864-8.688A29.227 29.227 0 0 0 123.476 3.81c-2.783-1.416-5.634-2.314-8.697-2.864-3.016-.542-6.094-.722-9.144-.804-1.4-.038-2.801-.054-4.202-.063C99.77.068 98.107.07 96.444.07L77.135 0H62.694L43.726.07c-1.666 0-3.332-.002-4.998.008-1.404.01-2.807.025-4.21.063-3.058.082-6.142.262-9.166.805-3.067.55-5.922 1.447-8.709 2.862a29.293 29.293 0 0 0-7.419 5.377 29.223 29.223 0 0 0-5.389 7.4c-1.42 2.78-2.32 5.63-2.871 8.691-.543 3.016-.723 6.091-.806 9.14-.038 1.4-.054 2.8-.062 4.2C.086 40.277 0 42.342 0 44.004v33.3l.086 19.102c0 1.665 0 3.33.01 4.994a200.6 200.6 0 0 0 .062 4.205c.083 3.054.263 6.135.807 9.155.551 3.064 1.451 5.916 2.87 8.7a29.294 29.294 0 0 0 12.807 12.794c2.788 1.418 5.645 2.317 8.714 2.868 3.022.542 6.105.722 9.162.804 1.403.038 2.806.054 4.21.063 1.666.01 3.332.009 4.998.009l19.14.001h14.477l19.101-.001c1.663 0 3.326.001 4.989-.009a202.92 202.92 0 0 0 4.202-.063c3.052-.082 6.13-.262 9.148-.805 3.061-.551 5.911-1.45 8.692-2.867a29.215 29.215 0 0 0 7.405-5.384 29.22 29.22 0 0 0 5.378-7.409c1.417-2.785 2.315-5.639 2.866-8.704.542-3.02.722-6.099.804-9.152.038-1.402.054-2.804.062-4.205.011-1.665.01-3.33.01-4.993l-.001-19.103V62.694L140 43.602"
clip-rule="evenodd"
/><path
fill="#000"
fill-rule="evenodd"
d="M39.375 40.188h8.313a6.25 6.25 0 0 1 6.25 6.25v24.25h16.25v8.75h-18.75a6.25 6.25 0 0 1-6.25-6.25v-24.25h-5.813v-8.75zm63.563 6.25v6.5h-8.75v-4h-6.876v30.5h-8.75v-30.5h-6.874v4h-8.75v-6.5a6.25 6.25 0 0 1 6.25-6.25h27.5a6.25 6.25 0 0 1 6.25 6.25z"
clip-rule="evenodd"
/><path
fill="#239AFF"
d="M35.319 102.906l-8.138-5.812c2.39-3.347 4.857-5.936 7.452-7.753 2.884-2.018 5.948-3.091 9.117-3.091 2.942 0 5.491.714 7.768 2.08a17.622 17.622 0 0 1 2.615 1.94c.589.518 1.009.926 1.903 1.82 1.355 1.354 1.917 1.851 2.591 2.255.731.439 1.503.655 2.623.655 1.121 0 1.896-.217 2.631-.657.677-.405 1.245-.905 2.6-2.257l.012-.012c.89-.888 1.314-1.299 1.902-1.817a17.643 17.643 0 0 1 2.61-1.933c2.273-1.362 4.814-2.074 7.745-2.074s5.472.712 7.745 2.074c.916.55 1.758 1.183 2.61 1.933.589.518 1.013.929 1.902 1.817l.013.012c1.354 1.352 1.922 1.852 2.599 2.257.735.44 1.51.657 2.631.657.998 0 2.1-.386 3.383-1.284 1.572-1.1 3.272-2.886 5.048-5.372l8.138 5.812c-2.391 3.347-4.857 5.936-7.452 7.753-2.884 2.018-5.948 3.091-9.117 3.091-2.941 0-5.49-.713-7.769-2.078a17.627 17.627 0 0 1-2.619-1.938c-.59-.519-1.015-.93-1.906-1.82l-.013-.013c-1.351-1.348-1.917-1.846-2.59-2.25-.728-.436-1.494-.651-2.603-.651-1.109 0-1.875.215-2.603.651-.673.404-1.239.902-2.59 2.25l-.012.013c-.892.89-1.317 1.301-1.907 1.82-.855.752-1.7 1.388-2.62 1.938C66.74 104.287 64.192 105 61.25 105c-2.942 0-5.49-.714-7.768-2.08a17.654 17.654 0 0 1-2.615-1.939c-.588-.519-1.009-.927-1.902-1.82-1.355-1.355-1.918-1.852-2.592-2.256-.731-.439-1.503-.655-2.623-.655-.998 0-2.1.386-3.383 1.284-1.572 1.1-3.272 2.886-5.048 5.372z"
/></g
><defs><clipPath id="clip0"><path fill="#fff" d="M0 0h140v140H0z" /></clipPath></defs></svg
>

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

@@ -15,6 +15,9 @@ export async function isDockerNetworkExists({ network }) {
return await prisma.destinationDocker.findFirst({ where: { network } }); return await prisma.destinationDocker.findFirst({ where: { network } });
} }
export async function isServiceSecretExists({ id, name }) {
return await prisma.serviceSecret.findFirst({ where: { name, serviceId: id } });
}
export async function isSecretExists({ id, name, isPRMRSecret }) { export async function isSecretExists({ id, name, isPRMRSecret }) {
return await prisma.secret.findFirst({ where: { name, applicationId: id, isPRMRSecret } }); return await prisma.secret.findFirst({ where: { name, applicationId: id, isPRMRSecret } });
} }

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
@@ -156,6 +158,43 @@ export const supportedServiceTypesAndVersions = [
ports: { ports: {
main: 80 main: 80
} }
},
{
name: 'languagetool',
fancyName: 'LanguageTool',
baseImage: 'silviof/docker-languagetool',
versions: ['latest'],
ports: {
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
}
} }
]; ];
@@ -180,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,6 +1,19 @@
import { encrypt, decrypt } from '$lib/crypto'; import { encrypt, decrypt } from '$lib/crypto';
import { prisma } from './common'; import { prisma } from './common';
export async function listServiceSecrets(serviceId: string) {
let secrets = await prisma.serviceSecret.findMany({
where: { serviceId },
orderBy: { createdAt: 'desc' }
});
secrets = secrets.map((secret) => {
secret.value = decrypt(secret.value);
return secret;
});
return secrets;
}
export async function listSecrets(applicationId: string) { export async function listSecrets(applicationId: string) {
let secrets = await prisma.secret.findMany({ let secrets = await prisma.secret.findMany({
where: { applicationId }, where: { applicationId },
@@ -14,6 +27,12 @@ export async function listSecrets(applicationId: string) {
return secrets; return secrets;
} }
export async function createServiceSecret({ id, name, value }) {
value = encrypt(value);
return await prisma.serviceSecret.create({
data: { name, value, service: { connect: { id } } }
});
}
export async function createSecret({ id, name, value, isBuildSecret, isPRMRSecret }) { export async function createSecret({ id, name, value, isBuildSecret, isPRMRSecret }) {
value = encrypt(value); value = encrypt(value);
return await prisma.secret.create({ return await prisma.secret.create({
@@ -21,10 +40,24 @@ export async function createSecret({ id, name, value, isBuildSecret, isPRMRSecre
}); });
} }
export async function updateServiceSecret({ id, name, value }) {
value = encrypt(value);
const found = await prisma.serviceSecret.findFirst({ where: { serviceId: id, name } });
if (found) {
return await prisma.serviceSecret.updateMany({
where: { serviceId: id, name },
data: { value }
});
} else {
return await prisma.serviceSecret.create({
data: { name, value, service: { connect: { id } } }
});
}
}
export async function updateSecret({ id, name, value, isBuildSecret, isPRMRSecret }) { export async function updateSecret({ id, name, value, isBuildSecret, isPRMRSecret }) {
value = encrypt(value); value = encrypt(value);
const found = await prisma.secret.findFirst({ where: { applicationId: id, name, isPRMRSecret } }); const found = await prisma.secret.findFirst({ where: { applicationId: id, name, isPRMRSecret } });
console.log(found);
if (found) { if (found) {
return await prisma.secret.updateMany({ return await prisma.secret.updateMany({
@@ -38,6 +71,10 @@ export async function updateSecret({ id, name, value, isBuildSecret, isPRMRSecre
} }
} }
export async function removeServiceSecret({ id, name }) {
return await prisma.serviceSecret.deleteMany({ where: { serviceId: id, name } });
}
export async function removeSecret({ id, name }) { export async function removeSecret({ id, name }) {
return await prisma.secret.deleteMany({ where: { applicationId: id, name } }); return await prisma.secret.deleteMany({ where: { applicationId: id, name } });
} }

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 '.';
@@ -19,7 +20,9 @@ export async function getService({ id, teamId }) {
plausibleAnalytics: true, plausibleAnalytics: true,
minio: true, minio: true,
vscodeserver: true, vscodeserver: true,
wordpress: true wordpress: true,
ghost: true,
serviceSecret: true
} }
}); });
@@ -42,6 +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) {
body.serviceSecret = body.serviceSecret.map((s) => {
s.value = decrypt(s.value);
return s;
});
}
return { ...body }; return { ...body };
} }
@@ -105,6 +120,51 @@ export async function configureServiceType({ id, type }) {
type type
} }
}); });
} else if (type === 'languagetool') {
await prisma.service.update({
where: { id },
data: {
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 }) {
@@ -125,7 +185,10 @@ 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 } });
}
export async function updateLanguageToolService({ 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 updateVaultWardenService({ id, fqdn, name }) { export async function updateVaultWardenService({ id, fqdn, name }) {
@@ -143,11 +206,20 @@ 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 } });
await prisma.wordpress.deleteMany({ where: { serviceId: id } }); await prisma.wordpress.deleteMany({ where: { serviceId: id } });
await prisma.serviceSecret.deleteMany({ where: { serviceId: id } });
await prisma.service.delete({ where: { id } }); await prisma.service.delete({ where: { 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}}
@@ -112,9 +112,15 @@ export async function haproxyInstance() {
} }
export async function configureHAProxy() { export async function configureHAProxy() {
const haproxy = await haproxyInstance();
try { try {
const haproxy = await haproxyInstance();
await checkHAProxy(haproxy); await checkHAProxy(haproxy);
} catch (error) {
return 'Error: HAProxy is not running';
}
try {
const data = { const data = {
applications: [], applications: [],
services: [], services: [],

View File

@@ -49,7 +49,12 @@ export async function completeTransaction(transactionId) {
} }
export async function deleteProxy({ id }) { export async function deleteProxy({ id }) {
const haproxy = await haproxyInstance(); const haproxy = await haproxyInstance();
await checkHAProxy(haproxy); try {
await checkHAProxy(haproxy);
} catch (error) {
return 'Error: HAProxy is not running';
}
let transactionId; let transactionId;
try { try {
await haproxy.get(`v2/services/haproxy/configuration/backends/${id}`).json(); await haproxy.get(`v2/services/haproxy/configuration/backends/${id}`).json();
@@ -175,11 +180,9 @@ export async function checkContainer(engine, container) {
const { stdout } = await asyncExecShell( const { stdout } = await asyncExecShell(
`DOCKER_HOST="${host}" docker inspect --format '{{json .State}}' ${container}` `DOCKER_HOST="${host}" docker inspect --format '{{json .State}}' ${container}`
); );
const parsedStdout = JSON.parse(stdout); const parsedStdout = JSON.parse(stdout);
const status = parsedStdout.Status; const status = parsedStdout.Status;
const isRunning = parsedStdout.Running; const isRunning = status === 'running' ? true : false;
if (status === 'exited' || status === 'created') { if (status === 'exited' || status === 'created') {
await asyncExecShell(`DOCKER_HOST="${host}" docker rm ${container}`); await asyncExecShell(`DOCKER_HOST="${host}" docker rm ${container}`);
} }

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

@@ -1,5 +1,5 @@
import { asyncExecShell, getDomain, getEngine } from '$lib/common'; import { asyncExecShell, getDomain, getEngine } from '$lib/common';
import { checkContainer } from '$lib/haproxy'; import { checkContainer, reloadHaproxy } from '$lib/haproxy';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { dev } from '$app/env'; import { dev } from '$app/env';
import cuid from 'cuid'; import cuid from 'cuid';
@@ -48,6 +48,17 @@ export async function letsEncrypt(domain, id = null, isCoolify = false) {
} }
} }
if (dualCerts) { if (dualCerts) {
let found = false;
try {
await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm -v "coolify-letsencrypt:/etc/letsencrypt" -v "coolify-ssl-certs:/app/ssl" alpine:latest sh -c "ls -1 /app/ssl/${wwwDomain}.pem"`
);
found = true;
} catch (error) {
//
}
if (found) return;
await asyncExecShell( await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:${randomPort} -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port ${randomPort} -d ${nakedDomain} -d ${wwwDomain} --expand --agree-tos --non-interactive --register-unsafely-without-email ${ `DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:${randomPort} -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port ${randomPort} -d ${nakedDomain} -d ${wwwDomain} --expand --agree-tos --non-interactive --register-unsafely-without-email ${
dev ? '--test-cert' : '' dev ? '--test-cert' : ''
@@ -56,7 +67,18 @@ export async function letsEncrypt(domain, id = null, isCoolify = false) {
await asyncExecShell( await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm -v "coolify-letsencrypt:/etc/letsencrypt" -v "coolify-ssl-certs:/app/ssl" alpine:latest sh -c "test -d /etc/letsencrypt/live/${nakedDomain}/ && cat /etc/letsencrypt/live/${nakedDomain}/fullchain.pem /etc/letsencrypt/live/${nakedDomain}/privkey.pem > /app/ssl/${nakedDomain}.pem || cat /etc/letsencrypt/live/${wwwDomain}/fullchain.pem /etc/letsencrypt/live/${wwwDomain}/privkey.pem > /app/ssl/${wwwDomain}.pem"` `DOCKER_HOST=${host} docker run --rm -v "coolify-letsencrypt:/etc/letsencrypt" -v "coolify-ssl-certs:/app/ssl" alpine:latest sh -c "test -d /etc/letsencrypt/live/${nakedDomain}/ && cat /etc/letsencrypt/live/${nakedDomain}/fullchain.pem /etc/letsencrypt/live/${nakedDomain}/privkey.pem > /app/ssl/${nakedDomain}.pem || cat /etc/letsencrypt/live/${wwwDomain}/fullchain.pem /etc/letsencrypt/live/${wwwDomain}/privkey.pem > /app/ssl/${wwwDomain}.pem"`
); );
await reloadHaproxy(host);
} else { } else {
let found = false;
try {
await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm -v "coolify-letsencrypt:/etc/letsencrypt" -v "coolify-ssl-certs:/app/ssl" alpine:latest sh -c "ls -1 /app/ssl/${domain}.pem"`
);
found = true;
} catch (error) {
//
}
if (found) return;
await asyncExecShell( await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:${randomPort} -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port ${randomPort} -d ${domain} --expand --agree-tos --non-interactive --register-unsafely-without-email ${ `DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:${randomPort} -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port ${randomPort} -d ${domain} --expand --agree-tos --non-interactive --register-unsafely-without-email ${
dev ? '--test-cert' : '' dev ? '--test-cert' : ''
@@ -65,6 +87,7 @@ export async function letsEncrypt(domain, id = null, isCoolify = false) {
await asyncExecShell( await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm -v "coolify-letsencrypt:/etc/letsencrypt" -v "coolify-ssl-certs:/app/ssl" alpine:latest sh -c "cat /etc/letsencrypt/live/${domain}/fullchain.pem /etc/letsencrypt/live/${domain}/privkey.pem > /app/ssl/${domain}.pem"` `DOCKER_HOST=${host} docker run --rm -v "coolify-letsencrypt:/etc/letsencrypt" -v "coolify-ssl-certs:/app/ssl" alpine:latest sh -c "cat /etc/letsencrypt/live/${domain}/fullchain.pem /etc/letsencrypt/live/${domain}/privkey.pem > /app/ssl/${domain}.pem"`
); );
await reloadHaproxy(host);
} }
} catch (error) { } catch (error) {
if (error.code !== 0) { if (error.code !== 0) {
@@ -76,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({
@@ -115,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();
@@ -145,9 +183,10 @@ export async function generateSSLCerts() {
if (ssls.length > 0) { if (ssls.length > 0) {
for (const ssl of ssls) { for (const ssl of ssls) {
if (!dev) { if (!dev) {
console.log('Checking SSL for', ssl.domain);
await letsEncrypt(ssl.domain, ssl.id, ssl.isCoolify); await letsEncrypt(ssl.domain, ssl.id, ssl.isCoolify);
} else { } else {
console.log('Generate ssl for', ssl.domain); console.log('Checking SSL for', ssl.domain);
} }
} }
} }

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

@@ -1,12 +1,33 @@
import { dev } from '$app/env'; import { dev } from '$app/env';
import { asyncExecShell, getEngine } from '$lib/common'; import { asyncExecShell, getEngine, version } from '$lib/common';
import { prisma } from '$lib/database'; import { prisma } from '$lib/database';
import { defaultProxyImageHttp, defaultProxyImageTcp } from '$lib/haproxy'; import { defaultProxyImageHttp, defaultProxyImageTcp } from '$lib/haproxy';
export default async function () { export default async function () {
const destinationDockers = await prisma.destinationDocker.findMany(); const destinationDockers = await prisma.destinationDocker.findMany();
for (const destinationDocker of destinationDockers) { for (const destinationDocker of destinationDockers) {
const host = getEngine(destinationDocker.engine); const host = getEngine(destinationDocker.engine);
// Cleanup old coolify images
try {
let { stdout: images } = await asyncExecShell(
`DOCKER_HOST=${host} docker images coollabsio/coolify --filter before="coollabsio/coolify:${version}" -q | xargs `
);
images = images.trim();
if (images) {
await asyncExecShell(`DOCKER_HOST=${host} docker rmi -f ${images}`);
}
} catch (error) {
console.log(error);
}
try {
await asyncExecShell(`DOCKER_HOST=${host} docker container prune -f`);
} catch (error) {
console.log(error);
}
try {
await asyncExecShell(`DOCKER_HOST=${host} docker image prune -f --filter "until=2h"`);
} catch (error) {
console.log(error);
}
// Tagging images with labels // Tagging images with labels
// try { // try {
// const images = [ // const images = [
@@ -30,16 +51,6 @@ export default async function () {
// } // }
// } // }
// } catch (error) {} // } catch (error) {}
try {
await asyncExecShell(`DOCKER_HOST=${host} docker container prune -f`);
} catch (error) {
console.log(error);
}
try {
await asyncExecShell(`DOCKER_HOST=${host} docker image prune -f`);
} catch (error) {
console.log(error);
}
// if (!dev) { // if (!dev) {
// // Cleanup images that are not managed by coolify // // Cleanup images that are not managed by coolify
// try { // try {

View File

@@ -86,8 +86,8 @@ 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: 60000 } }); await queue.ssl.add('ssl', {}, { repeat: { every: dev ? 10000 : 60000 } });
await queue.cleanup.add('cleanup', {}, { repeat: { every: 600000 } }); 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

@@ -1,4 +1,3 @@
import { dev } from '$app/env';
import { ErrorHandler } from '$lib/database'; import { ErrorHandler } from '$lib/database';
import { configureHAProxy } from '$lib/haproxy/configuration'; import { configureHAProxy } from '$lib/haproxy/configuration';

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

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

View File

@@ -63,13 +63,11 @@
const data = await get(`/update.json`); const data = await get(`/update.json`);
if (overrideVersion || data?.isUpdateAvailable) { if (overrideVersion || data?.isUpdateAvailable) {
latestVersion = overrideVersion || data.latestVersion; latestVersion = overrideVersion || data.latestVersion;
console.log('checking update'); if (overrideVersion) {
const { exists } = await post(`/update.json`, { isUpdateAvailable = true;
type: 'check', } else {
latestVersion, isUpdateAvailable = data.isUpdateAvailable;
overrideVersion }
});
isUpdateAvailable = exists;
} }
} catch (error) { } catch (error) {
} finally { } finally {

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 window.location.assign(`/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);
} }
@@ -111,8 +120,6 @@
return window.location.reload(); 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

@@ -51,9 +51,9 @@
build.took = data.builds[0].took; build.took = data.builds[0].took;
build.since = data.builds[0].since; build.since = data.builds[0].since;
} }
return build; return build;
}); });
return;
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
} }
@@ -120,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

@@ -25,7 +25,7 @@
define('WP_ALLOW_MULTISITE', true); define('WP_ALLOW_MULTISITE', true);
define('MULTISITE', true); define('MULTISITE', true);
define('SUBDOMAIN_INSTALL', false);` define('SUBDOMAIN_INSTALL', false);`
: null}>{service.wordpress.extraConfig || 'N/A'}</textarea : 'N/A'}>{service.wordpress.extraConfig}</textarea
> >
</div> </div>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">

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: {
@@ -57,13 +58,13 @@
</script> </script>
<script> <script>
import { session } from '$app/stores'; import { page, session } from '$app/stores';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import DeleteIcon from '$lib/components/DeleteIcon.svelte'; import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import Loading from '$lib/components/Loading.svelte'; import Loading from '$lib/components/Loading.svelte';
import { del, post } from '$lib/api'; import { del, post } from '$lib/api';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { onMount } from 'svelte'; const { id } = $page.params;
export let service; export let service;
export let isRunning; export let isRunning;
@@ -110,23 +111,6 @@
loading = false; loading = false;
} }
} }
// onMount(async () => {
// if (
// service.type &&
// service.destinationDockerId &&
// service.version &&
// service.fqdn &&
// !isRunning
// ) {
// try {
// await post(`/services/${service.id}/${service.type}/stop.json`, {});
// } catch ({ error }) {
// return errorNotification(error);
// } finally {
// loading = false;
// }
// }
// });
</script> </script>
<nav class="nav-side"> <nav class="nav-side">
@@ -185,6 +169,76 @@
</svg> </svg>
</button> </button>
{/if} {/if}
<div class="border border-stone-700 h-8" />
{/if}
{#if service.type && service.destinationDockerId && service.version}
<a
href="/services/{id}"
sveltekit:prefetch
class="hover:text-yellow-500 rounded"
class:text-yellow-500={$page.url.pathname === `/services/${id}`}
class:bg-coolgray-500={$page.url.pathname === `/services/${id}`}
>
<button
title="Configurations"
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
data-tooltip="Configurations"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="4" y="8" width="4" height="4" />
<line x1="6" y1="4" x2="6" y2="8" />
<line x1="6" y1="12" x2="6" y2="20" />
<rect x="10" y="14" width="4" height="4" />
<line x1="12" y1="4" x2="12" y2="14" />
<line x1="12" y1="18" x2="12" y2="20" />
<rect x="16" y="5" width="4" height="4" />
<line x1="18" y1="4" x2="18" y2="5" />
<line x1="18" y1="9" x2="18" y2="20" />
</svg></button
></a
>
<a
href="/services/{id}/secrets"
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/services/${id}/secrets`}
class:bg-coolgray-500={$page.url.pathname === `/services/${id}/secrets`}
>
<button
title="Secrets"
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
data-tooltip="Secrets"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3"
/>
<circle cx="12" cy="11" r="1" />
<line x1="12" y1="12" x2="12" y2="14.5" />
</svg></button
></a
>
<div class="border border-stone-700 h-8" />
{/if} {/if}
<button <button
on:click={deleteService} on:click={deleteService}

View File

@@ -37,6 +37,10 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
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 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');
@@ -74,6 +78,14 @@
<Wordpress isAbsolute /> <Wordpress isAbsolute />
{:else if type.name === 'vaultwarden'} {:else if type.name === 'vaultwarden'}
<VaultWarden isAbsolute /> <VaultWarden isAbsolute />
{:else if type.name === 'languagetool'}
<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

@@ -35,10 +35,13 @@
import VsCodeServer from '$lib/components/svg/services/VSCodeServer.svelte'; import VsCodeServer from '$lib/components/svg/services/VSCodeServer.svelte';
import Wordpress from '$lib/components/svg/services/Wordpress.svelte'; import Wordpress from '$lib/components/svg/services/Wordpress.svelte';
import Services from './_Services/_Services.svelte'; import Services from './_Services/_Services.svelte';
import { getDomain } from '$lib/components/common';
import VaultWarden from '$lib/components/svg/services/VaultWarden.svelte'; import VaultWarden from '$lib/components/svg/services/VaultWarden.svelte';
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 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;
@@ -105,6 +108,22 @@
<a href="https://github.com/dani-garcia/vaultwarden" target="_blank"> <a href="https://github.com/dani-garcia/vaultwarden" target="_blank">
<VaultWarden /> <VaultWarden />
</a> </a>
{:else if service.type === 'languagetool'}
<a href="https://languagetool.org/dev" target="_blank">
<LanguageTool />
</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

@@ -4,25 +4,17 @@ 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 };
let { name, fqdn, port, buildCommand, startCommand, installCommand } = await event.request.json(); const { id } = event.params;
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.updateLanguageToolService({ 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,78 @@
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}-ngrams:/ngrams`,
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],
environment: config.environmentVariables,
restart: 'always',
volumes: [config.volume],
labels: makeLabelForServices('languagetool')
}
},
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

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

@@ -6,7 +6,7 @@ import type { RequestHandler } from '@sveltejs/kit';
import { startHttpProxy } from '$lib/haproxy'; import { startHttpProxy } from '$lib/haproxy';
import getPort, { portNumbers } from 'get-port'; import getPort, { portNumbers } from 'get-port';
import { getDomain } from '$lib/components/common'; import { getDomain } from '$lib/components/common';
import { ErrorHandler } from '$lib/database'; import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
@@ -23,7 +23,8 @@ export const post: RequestHandler = async (event) => {
fqdn, fqdn,
destinationDockerId, destinationDockerId,
destinationDocker, destinationDocker,
minio: { rootUser, rootUserPassword } minio: { rootUser, rootUserPassword },
serviceSecret
} = service; } = service;
const data = await db.prisma.setting.findFirst(); const data = await db.prisma.setting.findFirst();
@@ -38,9 +39,10 @@ export const post: RequestHandler = async (event) => {
const apiPort = 9000; const apiPort = 9000;
const { workdir } = await createDirectories({ repository: type, buildId: id }); const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
const config = { const config = {
image: `minio/minio:${version}`, image: `${image}:${version}`,
volume: `${id}-minio-data:/data`, volume: `${id}-minio-data:/data`,
environmentVariables: { environmentVariables: {
MINIO_ROOT_USER: rootUser, MINIO_ROOT_USER: rootUser,
@@ -48,12 +50,17 @@ export const post: RequestHandler = async (event) => {
MINIO_BROWSER_REDIRECT_URL: fqdn MINIO_BROWSER_REDIRECT_URL: fqdn
} }
}; };
if (serviceSecret.length > 0) {
serviceSecret.forEach((secret) => {
config.environmentVariables[secret.name] = secret.value;
});
}
const composeFile = { const composeFile = {
version: '3.8', version: '3.8',
services: { services: {
[id]: { [id]: {
container_name: id, container_name: id,
image: `minio/minio:${version}`, image: config.image,
command: `server /data --console-address ":${consolePort}"`, command: `server /data --console-address ":${consolePort}"`,
environment: config.environmentVariables, environment: config.environmentVariables,
networks: [network], networks: [network],
@@ -69,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

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

@@ -3,7 +3,7 @@ import * as db from '$lib/database';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler } from '$lib/database'; import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
@@ -14,19 +14,30 @@ export const post: RequestHandler = async (event) => {
try { try {
const service = await db.getService({ id, teamId }); const service = await db.getService({ id, teamId });
const { type, version, destinationDockerId, destinationDocker } = service; const { type, version, destinationDockerId, destinationDocker, serviceSecret } = service;
const network = destinationDockerId && destinationDocker.network; const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine); const host = getEngine(destinationDocker.engine);
const { workdir } = await createDirectories({ repository: type, buildId: id }); const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
const config = {
image: `${image}:${version}`,
environmentVariables: {}
};
if (serviceSecret.length > 0) {
serviceSecret.forEach((secret) => {
config.environmentVariables[secret.name] = secret.value;
});
}
const composeFile = { const composeFile = {
version: '3.8', version: '3.8',
services: { services: {
[id]: { [id]: {
container_name: id, container_name: id,
image: `nocodb/nocodb:${version}`, image: config.image,
networks: [network], networks: [network],
environment: config.environmentVariables,
restart: 'always', restart: 'always',
labels: makeLabelForServices('nocodb') labels: makeLabelForServices('nocodb')
} }
@@ -41,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

@@ -3,7 +3,7 @@ import * as db from '$lib/database';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler } from '$lib/database'; import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
@@ -20,6 +20,7 @@ export const post: RequestHandler = async (event) => {
fqdn, fqdn,
destinationDockerId, destinationDockerId,
destinationDocker, destinationDocker,
serviceSecret,
plausibleAnalytics: { plausibleAnalytics: {
id: plausibleDbId, id: plausibleDbId,
username, username,
@@ -31,10 +32,11 @@ export const post: RequestHandler = async (event) => {
secretKeyBase secretKeyBase
} }
} = service; } = service;
const image = getServiceImage(type);
const config = { const config = {
plausibleAnalytics: { plausibleAnalytics: {
image: `plausible/analytics:${version}`, image: `${image}:${version}`,
environmentVariables: { environmentVariables: {
ADMIN_USER_EMAIL: email, ADMIN_USER_EMAIL: email,
ADMIN_USER_NAME: username, ADMIN_USER_NAME: username,
@@ -68,6 +70,11 @@ export const post: RequestHandler = async (event) => {
} }
} }
}; };
if (serviceSecret.length > 0) {
serviceSecret.forEach((secret) => {
config.plausibleAnalytics.environmentVariables[secret.name] = secret.value;
});
}
const network = destinationDockerId && destinationDocker.network; const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine); const host = getEngine(destinationDocker.engine);
@@ -151,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,87 @@
<script>
export let name = '';
export let value = '';
export let isNewSecret = false;
import { page } from '$app/stores';
import { del, post } from '$lib/api';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
const { id } = $page.params;
async function removeSecret() {
try {
await del(`/services/${id}/secrets.json`, { name });
dispatch('refresh');
if (isNewSecret) {
name = '';
value = '';
}
} catch ({ error }) {
return errorNotification(error);
}
}
async function saveSecret(isNew = false) {
if (!name) return errorNotification('Name is required.');
if (!value) return errorNotification('Value is required.');
try {
await post(`/services/${id}/secrets.json`, {
name,
value,
isNew
});
dispatch('refresh');
if (isNewSecret) {
name = '';
value = '';
}
toast.push('Secret saved.');
} catch ({ error }) {
return errorNotification(error);
}
}
</script>
<td>
<input
id={isNewSecret ? 'secretName' : 'secretNameNew'}
bind:value={name}
required
placeholder="EXAMPLE_VARIABLE"
class=" border border-dashed border-coolgray-300"
readonly={!isNewSecret}
class:bg-transparent={!isNewSecret}
class:cursor-not-allowed={!isNewSecret}
/>
</td>
<td>
<CopyPasswordField
id={isNewSecret ? 'secretValue' : 'secretValueNew'}
name={isNewSecret ? 'secretValue' : 'secretValueNew'}
isPasswordField={true}
bind:value
required
placeholder="J$#@UIO%HO#$U%H"
/>
</td>
<td>
{#if isNewSecret}
<div class="flex items-center justify-center">
<button class="bg-green-600 hover:bg-green-500" on:click={() => saveSecret(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={() => saveSecret(false)}>Set</button>
</div>
<div class="flex justify-center items-end">
<button class="bg-red-600 hover:bg-red-500" on:click={removeSecret}>Remove</button>
</div>
</div>
{/if}
</td>

View File

@@ -0,0 +1,70 @@
import { getTeam, 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 { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
try {
const secrets = await db.listServiceSecrets(id);
return {
status: 200,
body: {
secrets: secrets.sort((a, b) => {
return ('' + a.name).localeCompare(b.name);
})
}
};
} 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 { name, value, isBuildSecret, isPRMRSecret, isNew } = await event.request.json();
try {
if (isNew) {
const found = await db.isServiceSecretExists({ id, name });
if (found) {
throw {
error: `Secret ${name} already exists.`
};
} else {
await db.createServiceSecret({ id, name, value });
return {
status: 201
};
}
} else {
await db.updateServiceSecret({ id, name, value });
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 { name } = await event.request.json();
try {
await db.removeServiceSecret({ id, name });
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
}
};

View File

@@ -0,0 +1,67 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params, stuff }) => {
let endpoint = `/services/${params.id}/secrets.json`;
const res = await fetch(endpoint);
if (res.ok) {
return {
props: {
service: stuff.service,
...(await res.json())
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${endpoint}`)
};
};
</script>
<script lang="ts">
export let secrets;
export let service;
import Secret from './_Secret.svelte';
import { getDomain } from '$lib/components/common';
import { page } from '$app/stores';
import { get } from '$lib/api';
const { id } = $page.params;
async function refreshSecrets() {
const data = await get(`/services/${id}/secrets.json`);
secrets = [...data.secrets];
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">
Secrets {#if service.fqdn}
<a href={service.fqdn} target="_blank">{getDomain(service.fqdn)}</a>
{/if}
</div>
</div>
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4">
<table class="mx-auto border-separate text-left">
<thead>
<tr class="h-12">
<th scope="col">Name</th>
<th scope="col">Value</th>
<th scope="col" class="w-96 text-center">Action</th>
</tr>
</thead>
<tbody>
{#each secrets as secret}
{#key secret.id}
<tr>
<Secret name={secret.name} value={secret.value} on:refresh={refreshSecrets} />
</tr>
{/key}
{/each}
<tr>
<Secret isNewSecret on:refresh={refreshSecrets} />
</tr>
</tbody>
</table>
</div>

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);
}
};

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