Compare commits

...

110 Commits

Author SHA1 Message Date
Andras Bacsai
fdce70937f Merge pull request #572 from coollabsio/next
v3.8.5
2022-08-27 13:46:10 +02:00
Andras Bacsai
2060619e5b fix: again 2022-08-27 11:42:51 +00:00
Andras Bacsai
d7fd1fc65b fix: next/nuxt deployment type 2022-08-27 11:39:04 +00:00
Andras Bacsai
6760d7e776 fix: whitelabeled icon 2022-08-27 10:33:03 +00:00
Andras Bacsai
d61187c836 fix: white labeled icon on navbar 2022-08-27 10:30:07 +00:00
Andras Bacsai
f9932f9fee fix: process 2022-08-27 10:22:58 +00:00
Andras Bacsai
c003e56c03 fix: typo 2022-08-27 10:22:36 +00:00
Andras Bacsai
4d94106bff fix: copy all files during install process 2022-08-27 10:22:03 +00:00
Andras Bacsai
3bd2d4f868 Merge branch 'main' into next 2022-08-27 08:29:32 +00:00
Andras Bacsai
75b37ea0dc chore: version++ 2022-08-27 08:24:38 +00:00
Andras Bacsai
c356d0455d Update staging-release.yml 2022-08-27 10:19:50 +02:00
Andras Bacsai
278f75e70c Update production-release.yml 2022-08-27 10:19:32 +02:00
Andras Bacsai
2c7c5a3dc3 Merge pull request #571 from coollabsio/next
v3.8.4
2022-08-27 09:57:16 +02:00
Andras Bacsai
806ffacedd remove unnecessary lines 2022-08-27 07:56:38 +00:00
Andras Bacsai
93246f80c4 fix: pr deployments + remove public gits 2022-08-27 07:46:54 +00:00
Andras Bacsai
e9723d3f22 fix: cleanup build cache as well 2022-08-27 07:46:30 +00:00
Andras Bacsai
6baec7277f fix: decrypt secrets 2022-08-27 07:46:20 +00:00
Andras Bacsai
d43554d290 fix: queue cleanup 2022-08-27 07:46:06 +00:00
Andras Bacsai
463fee429b ui: fixes 2022-08-26 18:56:05 +00:00
Andras Bacsai
570b286ef9 fix: team switching 2022-08-26 18:44:51 +00:00
Andras Bacsai
4c5e71f33c fix: delete team while it is active 2022-08-26 18:39:13 +00:00
Andras Bacsai
3884483bca ui: dashbord fixes 2022-08-26 18:25:20 +00:00
Andras Bacsai
fc1a89cc63 fix: UI thinkgs 2022-08-26 17:11:54 +00:00
Andras Bacsai
c471eed808 Merge pull request #568 from coollabsio/next
v3.8.3
2022-08-26 18:01:55 +02:00
Andras Bacsai
35450dfc8f fix: secrets decryption 2022-08-26 16:00:36 +00:00
Andras Bacsai
5360c60f3d Merge pull request #565 from coollabsio/next
v3.8.2
2022-08-26 15:22:27 +02:00
Andras Bacsai
f60c640dc6 ui: fix 2022-08-26 13:28:54 +02:00
Andras Bacsai
91bb323e84 ui fixes 2022-08-26 13:22:43 +02:00
Andras Bacsai
cf9e122bd2 ui: fixes 2022-08-26 13:17:35 +02:00
Andras Bacsai
7528ca18d8 ui: fixes
feat: restart coolify button
2022-08-26 12:46:20 +02:00
Andras Bacsai
05ee35b6bc typo 2022-08-26 12:07:21 +02:00
Andras Bacsai
0e8b069781 ui: fine-tune 2022-08-26 10:29:45 +02:00
Andras Bacsai
3b95d7278d ui: dashboard fine-tunes 2022-08-26 10:22:50 +02:00
Andras Bacsai
7691706295 fix lockfile 2022-08-26 10:00:48 +02:00
Andras Bacsai
2af65ee946 Merge pull request #563 from kaname-png/rework-home
feat(ui): rework home UI and with responsive design
2022-08-26 10:00:06 +02:00
Andras Bacsai
279e1fd9c5 Merge branch 'next' into rework-home 2022-08-26 09:58:52 +02:00
Andras Bacsai
0f8f33e9fe remove unnecessary things 2022-08-26 09:49:17 +02:00
Andras Bacsai
12e91f1c6b fix 2022-08-26 09:33:50 +02:00
Andras Bacsai
f4f605867b hm 2022-08-26 09:09:41 +02:00
Andras Bacsai
ac40abba2e Update README.md 2022-08-26 09:05:26 +02:00
Andras Bacsai
224604f2e7 fixes 2022-08-26 09:01:48 +02:00
Andras Bacsai
ee4360de3a fix: better worker system 2022-08-26 06:29:06 +00:00
Kaname
0af59b9602 Merge remote-tracking branch 'upstream/next' into rework-home 2022-08-26 02:39:55 +00:00
Kaname
ff8fe68f14 feat(ui): rework home UI and with responsive design 2022-08-26 01:40:46 +00:00
Andras Bacsai
71c15e0ff5 fix: worker 2022-08-25 12:49:09 +00:00
Andras Bacsai
6be1fbacde Merge branch 'main' of https://github.com/coollabsio/coolify into next 2022-08-25 12:02:30 +00:00
Andras Bacsai
be594dd49e revert test 2022-08-25 12:44:04 +02:00
Andras Bacsai
6b65d435fb hmn 2022-08-25 12:32:50 +02:00
Andras Bacsai
0dc5212066 Revert things 2022-08-25 12:25:46 +02:00
Andras Bacsai
7c8ffd510e asd 2022-08-25 11:59:56 +02:00
Andras Bacsai
8e6423e873 hmm 2022-08-25 11:54:23 +02:00
Andras Bacsai
c99f74b351 debug 2022-08-25 11:49:19 +02:00
Andras Bacsai
57b462223c test 2022-08-25 11:09:38 +02:00
Andras Bacsai
055db97273 hm 2022-08-25 10:53:04 +02:00
Andras Bacsai
c80ebf73ee hmm 2022-08-25 10:47:58 +02:00
Andras Bacsai
9b613294ae debug cpu usage 2022-08-25 10:43:01 +02:00
Andras Bacsai
8cb73e1680 hmm 2022-08-25 10:37:21 +02:00
Andras Bacsai
27dfa24cfb remove debug 2022-08-25 10:29:08 +02:00
Andras Bacsai
c53f0dbb30 fix: high cpu usage 2022-08-25 10:28:32 +02:00
Andras Bacsai
db16a357e8 debug cpu usage 2022-08-25 10:17:07 +02:00
Andras Bacsai
01e71958b2 fix: build queue system 2022-08-25 10:04:46 +02:00
Andras Bacsai
f379519d40 chore: version++ 2022-08-24 14:50:34 +02:00
Andras Bacsai
54a09958d5 fix: never stop deplyo queue 2022-08-24 11:16:49 +02:00
Andras Bacsai
3421de06d5 Merge pull request #558 from coollabsio/next
v3.8.1
2022-08-24 10:54:53 +02:00
Andras Bacsai
e44eb01396 fix: dashboard for admins 2022-08-24 10:45:58 +02:00
Andras Bacsai
313143586b fix: cancelling jobs 2022-08-24 10:35:28 +02:00
Andras Bacsai
c8ae72893a fix: clear queue on cancelling jobs 2022-08-24 09:39:24 +02:00
Andras Bacsai
cbc3735ca0 fix: ui buttons 2022-08-23 13:03:01 +02:00
Andras Bacsai
31fdbdf8c9 Merge pull request #554 from coollabsio/next
v3.8.0
2022-08-23 11:41:52 +02:00
Andras Bacsai
2741d0ab2a fix: show build log start/end 2022-08-23 11:36:14 +02:00
Andras Bacsai
79b0187b58 fix: stream build logs 2022-08-23 11:29:25 +02:00
Andras Bacsai
bf5659d0e2 fix: dashboard for non-root users 2022-08-23 10:19:57 +02:00
Andras Bacsai
f6314cab69 chore: version++ 2022-08-23 10:11:58 +02:00
Andras Bacsai
4f5fe3d383 feat: Searxng service 2022-08-23 10:11:38 +02:00
Andras Bacsai
1c720d587c fix: batch secret = 2022-08-23 08:57:15 +02:00
Andras Bacsai
c46dc99224 fix: exposedPort checker 2022-08-23 08:50:28 +02:00
Andras Bacsai
5e43d4f20d update packages 2022-08-23 08:49:41 +02:00
Andras Bacsai
359434bfd3 fix: cancel build after 5 seconds 2022-08-23 08:49:32 +02:00
Andras Bacsai
e755a2d4ec fix: port checker 2022-08-22 19:30:09 +00:00
Andras Bacsai
0b416cd03e Merge pull request #549 from coollabsio/next
v3.7.0
2022-08-19 12:26:36 +02:00
Andras Bacsai
857e0f251b chore: version++ 2022-08-19 10:24:42 +00:00
Andras Bacsai
f040c7c742 fix: exposedPort is just optional 2022-08-19 10:18:27 +00:00
Andras Bacsai
2e82c9d312 Merge pull request #538 from MrSquaare/feature/glitchtip-service
feat: add GlitchTip service
2022-08-18 22:20:54 +02:00
Andras Bacsai
126923c33e update gitpod dockerfile 2022-08-18 20:11:48 +00:00
Andras Bacsai
26528d8bec gitpod configuration 2022-08-18 20:02:09 +00:00
Andras Bacsai
f99da111f7 Merge pull request #547 from coollabsio/next
v3.6.0
2022-08-18 21:50:29 +02:00
Andras Bacsai
8c30472472 Merge branch 'next' into feature/glitchtip-service 2022-08-18 21:47:06 +02:00
Andras Bacsai
11131ebe06 custom dockerfile for gitpod 2022-08-18 19:30:53 +00:00
Andras Bacsai
8dd80589d6 fix: bots without exposed ports 2022-08-18 19:27:38 +00:00
Andras Bacsai
51e27146f3 Update .gitpod.yml 2022-08-18 21:14:56 +02:00
Andras Bacsai
70717dcbe5 Merge pull request #546 from coollabsio/public_repos
Import public repositories feature and more
2022-08-18 20:56:08 +02:00
Andras Bacsai
51cba32d8d chore: version++ 2022-08-18 18:53:24 +00:00
Andras Bacsai
b9076714cf ui: fixes here and there 2022-08-18 18:53:02 +00:00
Andras Bacsai
b76caabd32 examples 2022-08-18 16:37:46 +02:00
Andras Bacsai
0922fd66a4 feat: force rebuild + env.PORT for port + public repo build 2022-08-18 16:33:32 +02:00
Andras Bacsai
4e7e9b2cfc feat: public repo deployment 2022-08-18 15:29:59 +02:00
Andras Bacsai
0c24134ac2 feat: import public repos (wip) 2022-08-18 11:53:42 +02:00
Andras Bacsai
f96e418dd6 Merge pull request #545 from coollabsio/next
v3.5.2
2022-08-17 14:06:51 +02:00
Andras Bacsai
1627415cca chore: version++ 2022-08-17 14:00:45 +02:00
Andras Bacsai
d047c91399 fix: show that Ghost values could be changed 2022-08-17 13:59:35 +02:00
Andras Bacsai
8bec5550cf fix: restart containers on-failure instead of always 2022-08-17 13:59:23 +02:00
Andras Bacsai
7a61ade4a0 Merge pull request #544 from coollabsio/next
v3.5.1
2022-08-17 13:43:24 +02:00
Andras Bacsai
0239be69e6 chore: version++ 2022-08-17 13:31:27 +02:00
Andras Bacsai
883bdc2879 fix: trim secrets 2022-08-17 13:31:06 +02:00
Andras Bacsai
c3457a4c8a fix: revert docker compose version to 2.6.1 2022-08-17 13:30:53 +02:00
Guillaume Bonnet
2962aa6166 Merge remote-tracking branch 'upstream/next' into feature/glitchtip-service
# Conflicts:
#	README.md
#	apps/api/src/routes/api/v1/services/handlers.ts
#	apps/ui/src/lib/components/svg/services/index.ts
2022-08-16 20:21:44 +02:00
Guillaume Bonnet
d80f760c92 fix: missing commas 2022-08-15 19:41:38 +00:00
Guillaume Bonnet
ce2c887469 Merge remote-tracking branch 'upstream/next' into feature/glitchtip-service
# Conflicts:
#	apps/api/prisma/schema.prisma
#	apps/api/src/lib/common.ts
#	apps/api/src/lib/serviceFields.ts
#	apps/api/src/routes/api/v1/services/handlers.ts
2022-08-15 21:30:53 +02:00
Guillaume Bonnet
4908463722 chore: add .pnpm-store in .gitignore 2022-08-15 09:57:01 +00:00
Guillaume Bonnet
26d0ef9ac9 feat: add GlitchTip service 2022-08-15 09:56:34 +00:00
97 changed files with 3197 additions and 1829 deletions

View File

@@ -34,4 +34,4 @@ jobs:
- uses: sarisia/actions-status-discord@v1 - uses: sarisia/actions-status-discord@v1
if: always() if: always()
with: with:
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_CHANNEL }} webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }}

View File

@@ -32,4 +32,4 @@ jobs:
- uses: sarisia/actions-status-discord@v1 - uses: sarisia/actions-status-discord@v1
if: always() if: always()
with: with:
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_CHANNEL }} webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}

4
.gitignore vendored
View File

@@ -1,5 +1,6 @@
.DS_Store .DS_Store
node_modules node_modules
.pnpm-store
build build
.svelte-kit .svelte-kit
package package
@@ -10,4 +11,5 @@ dist
client client
apps/api/db/*.db apps/api/db/*.db
local-serve local-serve
apps/api/db/migration.db-journal apps/api/db/migration.db-journal
apps/api/core*

4
.gitpod.Dockerfile vendored
View File

@@ -1,2 +1,2 @@
FROM gitpod/workspace-node:2022-06-20-19-54-55 FROM gitpod/workspace-full:2022-08-17-18-37-55
RUN (curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.27.0/pack-v0.27.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack) RUN brew install buildpacks/tap/pack

View File

@@ -1,11 +1,11 @@
# This configuration file was automatically generated by Gitpod. # This configuration file was automatically generated by Gitpod.
# Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file) # Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file)
# and commit this file to your remote git repository to share the goodness with others. # and commit this file to your remote git repository to share the goodness with others.
image: #image:
file: .gitpod.Dockerfile # file: .gitpod.Dockerfile
tasks: #tasks:
- init: pnpm install && pnpm db:push && pnpm db:seed # - init: pnpm install && pnpm db:push && pnpm db:seed
command: pnpm dev # command: pnpm dev
ports: ports:
- port: 3001 - port: 3001

View File

@@ -30,7 +30,8 @@ RUN mkdir -p ~/.docker/cli-plugins/
# https://download.docker.com/linux/static/stable/ # https://download.docker.com/linux/static/stable/
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-20.10.9 -o /usr/bin/docker RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-20.10.9 -o /usr/bin/docker
# https://github.com/docker/compose/releases # https://github.com/docker/compose/releases
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-compose-linux-2.7.0 -o ~/.docker/cli-plugins/docker-compose # Reverted to 2.6.1 because of this https://github.com/docker/compose/issues/9704. 2.9.0 still has a bug.
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-compose-linux-2.6.1 -o ~/.docker/cli-plugins/docker-compose
RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker
RUN (curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.27.0/pack-v0.27.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack) RUN (curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.27.0/pack-v0.27.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack)

View File

@@ -92,6 +92,7 @@ Deploy your resource to:
- [Umami](https://github.com/mikecao/umami) - [Umami](https://github.com/mikecao/umami)
- [Fider](https://fider.io) - [Fider](https://fider.io)
- [Hasura](https://hasura.io) - [Hasura](https://hasura.io)
- [GlitchTip](https://glitchtip.com)
## Migration from v1 ## Migration from v1
@@ -102,7 +103,7 @@ A fresh installation is necessary. v2 and v3 are not compatible with v1.
- Twitter: [@andrasbacsai](https://twitter.com/andrasbacsai) - Twitter: [@andrasbacsai](https://twitter.com/andrasbacsai)
- Telegram: [@andrasbacsai](https://t.me/andrasbacsai) - Telegram: [@andrasbacsai](https://t.me/andrasbacsai)
- Email: [andras@coollabs.io](mailto:andras@coollabs.io) - Email: [andras@coollabs.io](mailto:andras@coollabs.io)
- Discord: [Invitation](https://discord.gg/6rDM4fkymF) - Discord: [Invitation](https://coollabs.io/discord)
## Financial Contributors ## Financial Contributors

View File

@@ -16,26 +16,27 @@
"dependencies": { "dependencies": {
"@breejs/ts-worker": "2.0.0", "@breejs/ts-worker": "2.0.0",
"@fastify/autoload": "5.2.0", "@fastify/autoload": "5.2.0",
"@fastify/cookie": "7.3.1", "@fastify/cookie": "8.0.0",
"@fastify/cors": "8.1.0", "@fastify/cors": "8.1.0",
"@fastify/env": "4.1.0", "@fastify/env": "4.1.0",
"@fastify/jwt": "6.3.2", "@fastify/jwt": "6.3.2",
"@fastify/static": "6.5.0", "@fastify/static": "6.5.0",
"@iarna/toml": "2.2.5", "@iarna/toml": "2.2.5",
"@ladjs/graceful": "3.0.2",
"@prisma/client": "3.15.2", "@prisma/client": "3.15.2",
"axios": "0.27.2", "axios": "0.27.2",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"bree": "9.1.2", "bree": "9.1.2",
"cabin": "9.1.2", "cabin": "9.1.2",
"compare-versions": "4.1.3", "compare-versions": "4.1.4",
"cuid": "2.1.8", "cuid": "2.1.8",
"dayjs": "1.11.4", "dayjs": "1.11.5",
"dockerode": "3.3.3", "dockerode": "3.3.4",
"dotenv-extended": "2.9.0", "dotenv-extended": "2.9.0",
"fastify": "4.4.0", "execa": "6.1.0",
"fastify-plugin": "4.1.0", "fastify": "4.5.2",
"fastify-plugin": "4.2.0",
"generate-password": "1.7.0", "generate-password": "1.7.0",
"get-port": "6.1.2",
"got": "12.3.1", "got": "12.3.1",
"is-ip": "5.0.0", "is-ip": "5.0.0",
"is-port-reachable": "4.0.0", "is-port-reachable": "4.0.0",
@@ -43,19 +44,20 @@
"jsonwebtoken": "8.5.1", "jsonwebtoken": "8.5.1",
"node-forge": "1.3.1", "node-forge": "1.3.1",
"node-os-utils": "1.3.7", "node-os-utils": "1.3.7",
"p-queue": "7.3.0", "p-all": "4.0.0",
"p-throttle": "5.0.0",
"public-ip": "6.0.1", "public-ip": "6.0.1",
"ssh-config": "4.1.6", "ssh-config": "4.1.6",
"strip-ansi": "7.0.1", "strip-ansi": "7.0.1",
"unique-names-generator": "4.7.1" "unique-names-generator": "4.7.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "18.6.5", "@types/node": "18.7.13",
"@types/node-os-utils": "1.3.0", "@types/node-os-utils": "1.3.0",
"@typescript-eslint/eslint-plugin": "5.33.0", "@typescript-eslint/eslint-plugin": "5.35.1",
"@typescript-eslint/parser": "5.33.0", "@typescript-eslint/parser": "5.35.1",
"esbuild": "0.15.0", "esbuild": "0.15.5",
"eslint": "8.21.0", "eslint": "8.22.0",
"eslint-config-prettier": "8.5.0", "eslint-config-prettier": "8.5.0",
"eslint-plugin-prettier": "4.2.1", "eslint-plugin-prettier": "4.2.1",
"nodemon": "2.0.19", "nodemon": "2.0.19",

View File

@@ -0,0 +1,30 @@
-- CreateTable
CREATE TABLE "GlitchTip" (
"id" TEXT NOT NULL PRIMARY KEY,
"postgresqlUser" TEXT NOT NULL,
"postgresqlPassword" TEXT NOT NULL,
"postgresqlDatabase" TEXT NOT NULL,
"postgresqlPublicPort" INTEGER,
"secretKeyBase" TEXT,
"defaultEmail" TEXT NOT NULL,
"defaultUsername" TEXT NOT NULL,
"defaultPassword" TEXT NOT NULL,
"defaultEmailFrom" TEXT NOT NULL DEFAULT 'glitchtip@domain.tdl',
"emailSmtpHost" TEXT DEFAULT 'domain.tdl',
"emailSmtpPort" INTEGER DEFAULT 25,
"emailSmtpUser" TEXT,
"emailSmtpPassword" TEXT,
"emailSmtpUseTls" BOOLEAN DEFAULT false,
"emailSmtpUseSsl" BOOLEAN DEFAULT false,
"emailBackend" TEXT,
"mailgunApiKey" TEXT,
"sendgridApiKey" TEXT,
"enableOpenUserRegistration" BOOLEAN NOT NULL DEFAULT true,
"serviceId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "GlitchTip_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "GlitchTip_serviceId_key" ON "GlitchTip"("serviceId");

View File

@@ -0,0 +1,42 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_GitSource" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"forPublic" BOOLEAN NOT NULL DEFAULT false,
"type" TEXT,
"apiUrl" TEXT,
"htmlUrl" TEXT,
"customPort" INTEGER NOT NULL DEFAULT 22,
"organization" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"githubAppId" TEXT,
"gitlabAppId" TEXT,
CONSTRAINT "GitSource_githubAppId_fkey" FOREIGN KEY ("githubAppId") REFERENCES "GithubApp" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "GitSource_gitlabAppId_fkey" FOREIGN KEY ("gitlabAppId") REFERENCES "GitlabApp" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_GitSource" ("apiUrl", "createdAt", "customPort", "githubAppId", "gitlabAppId", "htmlUrl", "id", "name", "organization", "type", "updatedAt") SELECT "apiUrl", "createdAt", "customPort", "githubAppId", "gitlabAppId", "htmlUrl", "id", "name", "organization", "type", "updatedAt" FROM "GitSource";
DROP TABLE "GitSource";
ALTER TABLE "new_GitSource" RENAME TO "GitSource";
CREATE UNIQUE INDEX "GitSource_githubAppId_key" ON "GitSource"("githubAppId");
CREATE UNIQUE INDEX "GitSource_gitlabAppId_key" ON "GitSource"("gitlabAppId");
CREATE TABLE "new_ApplicationSettings" (
"id" TEXT NOT NULL PRIMARY KEY,
"applicationId" TEXT NOT NULL,
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
"debug" BOOLEAN NOT NULL DEFAULT false,
"previews" BOOLEAN NOT NULL DEFAULT false,
"autodeploy" BOOLEAN NOT NULL DEFAULT true,
"isBot" BOOLEAN NOT NULL DEFAULT false,
"isPublicRepository" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "ApplicationSettings_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_ApplicationSettings" ("applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "previews", "updatedAt") SELECT "applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "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,13 @@
-- CreateTable
CREATE TABLE "Searxng" (
"id" TEXT NOT NULL PRIMARY KEY,
"secretKey" TEXT NOT NULL,
"redisPassword" TEXT NOT NULL,
"serviceId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Searxng_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "Searxng_serviceId_key" ON "Searxng"("serviceId");

View File

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

View File

@@ -0,0 +1,24 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Build" (
"id" TEXT NOT NULL PRIMARY KEY,
"type" TEXT NOT NULL,
"applicationId" TEXT,
"destinationDockerId" TEXT,
"gitSourceId" TEXT,
"githubAppId" TEXT,
"gitlabAppId" TEXT,
"commit" TEXT,
"pullmergeRequestId" TEXT,
"forceRebuild" BOOLEAN NOT NULL DEFAULT false,
"sourceBranch" TEXT,
"branch" TEXT,
"status" TEXT DEFAULT 'queued',
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_Build" ("applicationId", "branch", "commit", "createdAt", "destinationDockerId", "gitSourceId", "githubAppId", "gitlabAppId", "id", "status", "type", "updatedAt") SELECT "applicationId", "branch", "commit", "createdAt", "destinationDockerId", "gitSourceId", "githubAppId", "gitlabAppId", "id", "status", "type", "updatedAt" FROM "Build";
DROP TABLE "Build";
ALTER TABLE "new_Build" RENAME TO "Build";
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -27,6 +27,7 @@ model Setting {
ipv4 String? ipv4 String?
ipv6 String? ipv6 String?
arch String? arch String?
concurrentBuilds Int @default(1)
} }
model User { model User {
@@ -119,16 +120,17 @@ model Application {
} }
model ApplicationSettings { model ApplicationSettings {
id String @id @default(cuid()) id String @id @default(cuid())
applicationId String @unique applicationId String @unique
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) autodeploy Boolean @default(true)
isBot Boolean @default(false) isBot Boolean @default(false)
createdAt DateTime @default(now()) isPublicRepository Boolean @default(false)
updatedAt DateTime @updatedAt createdAt DateTime @default(now())
application Application @relation(fields: [applicationId], references: [id]) updatedAt DateTime @updatedAt
application Application @relation(fields: [applicationId], references: [id])
} }
model ApplicationPersistentStorage { model ApplicationPersistentStorage {
@@ -196,6 +198,9 @@ model Build {
githubAppId String? githubAppId String?
gitlabAppId String? gitlabAppId String?
commit String? commit String?
pullmergeRequestId String?
forceRebuild Boolean @default(false)
sourceBranch String?
branch String? branch String?
status String? @default("queued") status String? @default("queued")
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@@ -238,6 +243,7 @@ model SshKey {
model GitSource { model GitSource {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
forPublic Boolean @default(false)
type String? type String?
apiUrl String? apiUrl String?
htmlUrl String? htmlUrl String?
@@ -314,33 +320,34 @@ model DatabaseSettings {
} }
model Service { model Service {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
fqdn String? fqdn String?
exposePort Int? exposePort Int?
dualCerts Boolean @default(false) dualCerts Boolean @default(false)
type String? type String?
version String? version String?
destinationDockerId String? destinationDockerId String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id]) destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
persistentStorage ServicePersistentStorage[]
serviceSecret ServiceSecret[]
teams Team[]
fider Fider? fider Fider?
ghost Ghost? ghost Ghost?
glitchTip GlitchTip?
hasura Hasura? hasura Hasura?
meiliSearch MeiliSearch? meiliSearch MeiliSearch?
minio Minio? minio Minio?
moodle Moodle? moodle Moodle?
plausibleAnalytics PlausibleAnalytics? plausibleAnalytics PlausibleAnalytics?
persistentStorage ServicePersistentStorage[]
serviceSecret ServiceSecret[]
umami Umami? umami Umami?
vscodeserver Vscodeserver? vscodeserver Vscodeserver?
wordpress Wordpress? wordpress Wordpress?
appwrite Appwrite? appwrite Appwrite?
searxng Searxng?
teams Team[]
} }
model PlausibleAnalytics { model PlausibleAnalytics {
@@ -515,3 +522,40 @@ model Appwrite {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id]) service Service @relation(fields: [serviceId], references: [id])
} }
model GlitchTip {
id String @id @default(cuid())
postgresqlUser String
postgresqlPassword String
postgresqlDatabase String
postgresqlPublicPort Int?
secretKeyBase String?
defaultEmail String
defaultUsername String
defaultPassword String
defaultEmailFrom String @default("glitchtip@domain.tdl")
emailSmtpHost String? @default("domain.tdl")
emailSmtpPort Int? @default(25)
emailSmtpUser String?
emailSmtpPassword String?
emailSmtpUseTls Boolean? @default(false)
emailSmtpUseSsl Boolean? @default(false)
emailBackend String?
mailgunApiKey String?
sendgridApiKey String?
enableOpenUserRegistration Boolean @default(true)
serviceId String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
}
model Searxng {
id String @id @default(cuid())
secretKey String
redisPassword String
serviceId String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
}

View File

@@ -66,6 +66,34 @@ async function main() {
} }
}); });
} }
const github = await prisma.gitSource.findFirst({
where: { htmlUrl: 'https://github.com', forPublic: true }
});
const gitlab = await prisma.gitSource.findFirst({
where: { htmlUrl: 'https://gitlab.com', forPublic: true }
});
if (!github) {
await prisma.gitSource.create({
data: {
apiUrl: 'https://api.github.com',
htmlUrl: 'https://github.com',
forPublic: true,
name: 'Github Public',
type: 'github'
}
});
}
if (!gitlab) {
await prisma.gitSource.create({
data: {
apiUrl: 'https://gitlab.com/api/v4',
htmlUrl: 'https://gitlab.com',
forPublic: true,
name: 'Gitlab Public',
type: 'gitlab'
}
});
}
} }
main() main()
.catch((e) => { .catch((e) => {

View File

@@ -5,11 +5,10 @@ import env from '@fastify/env';
import cookie from '@fastify/cookie'; import cookie from '@fastify/cookie';
import path, { join } from 'path'; import path, { join } from 'path';
import autoLoad from '@fastify/autoload'; import autoLoad from '@fastify/autoload';
import { asyncExecShell, isDev, listSettings, prisma, version } from './lib/common'; import { asyncExecShell, asyncSleep, isDev, listSettings, prisma, version } from './lib/common';
import { scheduler } from './lib/scheduler'; import { scheduler } from './lib/scheduler';
import axios from 'axios';
import compareVersions from 'compare-versions'; import compareVersions from 'compare-versions';
import Graceful from '@ladjs/graceful'
declare module 'fastify' { declare module 'fastify' {
interface FastifyInstance { interface FastifyInstance {
config: { config: {
@@ -104,49 +103,39 @@ fastify.listen({ port, host }, async (err: any, address: any) => {
} }
console.log(`Coolify's API is listening on ${host}:${port}`); console.log(`Coolify's API is listening on ${host}:${port}`);
await initServer(); await initServer();
await scheduler.start('deployApplication');
await scheduler.start('cleanupStorage');
await scheduler.start('cleanupPrismaEngines');
await scheduler.start('checkProxies');
// Check if no build is running const graceful = new Graceful({ brees: [scheduler] });
graceful.listen();
// Check for update
setInterval(async () => { setInterval(async () => {
const { isAutoUpdateEnabled } = await prisma.setting.findFirst(); if (!scheduler.workers.has('deployApplication')) {
if (isAutoUpdateEnabled) { scheduler.run('deployApplication');
const currentVersion = version;
const { data: versions } = await axios
.get(
`https://get.coollabs.io/versions.json`
, {
params: {
appId: process.env['COOLIFY_APP_ID'] || undefined,
version: currentVersion
}
})
const latestVersion = versions['coolify'].main.version;
const isUpdateAvailable = compareVersions(latestVersion, currentVersion);
if (isUpdateAvailable === 1) {
if (scheduler.workers.has('deployApplication')) {
scheduler.workers.get('deployApplication').postMessage("status:autoUpdater");
}
}
} }
if (!scheduler.workers.has('infrastructure')) {
scheduler.run('infrastructure');
}
}, 2000)
// autoUpdater
setInterval(async () => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:autoUpdater")
}, isDev ? 5000 : 60000 * 15) }, isDev ? 5000 : 60000 * 15)
// Cleanup storage // cleanupStorage
setInterval(async () => { setInterval(async () => {
if (scheduler.workers.has('deployApplication')) { scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupStorage")
scheduler.workers.get('deployApplication').postMessage("status:cleanupStorage"); }, isDev ? 6000 : 60000 * 10)
}
}, isDev ? 5000 : 60000 * 10) // checkProxies
setInterval(async () => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:checkProxies")
}, 10000)
// cleanupPrismaEngines
// setInterval(async () => {
// scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupPrismaEngines")
// }, 60000)
scheduler.on('worker deleted', async (name) => {
if (name === 'autoUpdater' || name === 'cleanupStorage') {
if (!scheduler.workers.has('deployApplication')) await scheduler.start('deployApplication');
}
});
await getArch(); await getArch();
await getIPAddress(); await getIPAddress();
}); });
@@ -170,6 +159,12 @@ async function initServer() {
try { try {
await asyncExecShell(`docker network create --attachable coolify`); await asyncExecShell(`docker network create --attachable coolify`);
} catch (error) { } } catch (error) { }
try {
const isOlder = compareVersions('3.8.1', version);
if (isOlder === 1) {
await prisma.build.updateMany({ where: { status: { in: ['running', 'queued'] } }, data: { status: 'failed' } });
}
} catch (error) { }
} }
async function getArch() { async function getArch() {
try { try {

View File

@@ -1,43 +0,0 @@
import axios from 'axios';
import compareVersions from 'compare-versions';
import { parentPort } from 'node:worker_threads';
import { asyncExecShell, asyncSleep, isDev, prisma, version } from '../lib/common';
(async () => {
if (parentPort) {
try {
const currentVersion = version;
const { data: versions } = await axios
.get(
`https://get.coollabs.io/versions.json`
, {
params: {
appId: process.env['COOLIFY_APP_ID'] || undefined,
version: currentVersion
}
})
const latestVersion = versions['coolify'].main.version;
const isUpdateAvailable = compareVersions(latestVersion, currentVersion);
if (isUpdateAvailable === 1) {
const activeCount = 0
if (activeCount === 0) {
if (!isDev) {
console.log(`Updating Coolify to ${latestVersion}.`);
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
await asyncExecShell(`env | grep COOLIFY > .env`);
await asyncExecShell(
`docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify && docker rm coolify && docker compose up -d --force-recreate"`
);
} else {
console.log('Updating (not really in dev mode).');
}
}
}
} catch (error) {
console.log(error);
} finally {
await prisma.$disconnect();
}
} else process.exit(0);
})();

View File

@@ -1,96 +0,0 @@
import { parentPort } from 'node:worker_threads';
import { prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, executeDockerCmd, listSettings } from '../lib/common';
import { checkContainer } from '../lib/docker';
(async () => {
if (parentPort) {
try {
const { arch } = await listSettings();
// Coolify Proxy local
const engine = '/var/run/docker.sock';
const localDocker = await prisma.destinationDocker.findFirst({
where: { engine, network: 'coolify' }
});
if (localDocker && localDocker.isCoolifyProxyUsed) {
// Remove HAProxy
const found = await checkContainer({ dockerId: localDocker.id, container: 'coolify-haproxy' });
if (found) {
await executeDockerCmd({
dockerId: localDocker.id,
command: `docker stop -t 0 coolify-haproxy && docker rm coolify-haproxy`
})
}
await startTraefikProxy(localDocker.id);
}
// TCP Proxies
const databasesWithPublicPort = await prisma.database.findMany({
where: { publicPort: { not: null } },
include: { settings: true, destinationDocker: true }
});
for (const database of databasesWithPublicPort) {
const { destinationDockerId, destinationDocker, publicPort, id } = database;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
const { privatePort } = generateDatabaseConfiguration(database, arch);
// Remove HAProxy
const found = await checkContainer({
dockerId: localDocker.id, container: `haproxy-for-${publicPort}`
});
if (found) {
await executeDockerCmd({
dockerId: localDocker.id,
command: `docker stop -t 0 haproxy-for-${publicPort} && docker rm haproxy-for-${publicPort}`
})
}
await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
}
}
const wordpressWithFtp = await prisma.wordpress.findMany({
where: { ftpPublicPort: { not: null } },
include: { service: { include: { destinationDocker: true } } }
});
for (const ftp of wordpressWithFtp) {
const { service, ftpPublicPort } = ftp;
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
// Remove HAProxy
const found = await checkContainer({ dockerId: localDocker.id, container: `haproxy-for-${ftpPublicPort}` });
if (found) {
await executeDockerCmd({
dockerId: localDocker.id,
command: `docker stop -t 0 haproxy -for-${ftpPublicPort} && docker rm haproxy-for-${ftpPublicPort}`
})
}
await startTraefikTCPProxy(destinationDocker, id, ftpPublicPort, 22, 'wordpressftp');
}
}
// HTTP Proxies
const minioInstances = await prisma.minio.findMany({
where: { publicPort: { not: null } },
include: { service: { include: { destinationDocker: true } } }
});
for (const minio of minioInstances) {
const { service, publicPort } = minio;
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
// Remove HAProxy
const found = await checkContainer({ dockerId: localDocker.id, container: `${id}-${publicPort}` });
if (found) {
await executeDockerCmd({
dockerId: localDocker.id,
command: `docker stop -t 0 ${id}-${publicPort} && docker rm ${id}-${publicPort} `
})
}
await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000);
}
}
} catch (error) {
} finally {
await prisma.$disconnect();
}
} else process.exit(0);
})();

View File

@@ -1,19 +0,0 @@
import { parentPort } from 'node:worker_threads';
import { asyncExecShell, isDev, prisma } from '../lib/common';
(async () => {
if (parentPort) {
if (!isDev) {
try {
const { stdout } = await asyncExecShell(`ps -ef | grep /app/prisma-engines/query-engine | grep -v grep | wc -l | xargs`)
if (stdout.trim() != null && stdout.trim() != '' && Number(stdout.trim()) > 1) {
await asyncExecShell(`killall -q -e /app/prisma-engines/query-engine -o 10m`)
}
} catch (error) {
console.log(error);
} finally {
await prisma.$disconnect();
}
}
} else process.exit(0);
})();

View File

@@ -1,60 +0,0 @@
import { parentPort } from 'node:worker_threads';
import { asyncExecShell, cleanupDockerStorage, executeDockerCmd, isDev, prisma, version } from '../lib/common';
(async () => {
if (parentPort) {
const destinationDockers = await prisma.destinationDocker.findMany();
let enginesDone = new Set()
for (const destination of destinationDockers) {
if (enginesDone.has(destination.engine) || enginesDone.has(destination.remoteIpAddress)) return
if (destination.engine) enginesDone.add(destination.engine)
if (destination.remoteIpAddress) enginesDone.add(destination.remoteIpAddress)
let lowDiskSpace = false;
try {
let stdout = null
if (!isDev) {
const output = await executeDockerCmd({ dockerId: destination.id, command: `CONTAINER=$(docker ps -lq | head -1) && docker exec $CONTAINER sh -c 'df -kPT /'` })
stdout = output.stdout;
} else {
const output = await asyncExecShell(
`df -kPT /`
);
stdout = output.stdout;
}
let lines = stdout.trim().split('\n');
let header = lines[0];
let regex =
/^Filesystem\s+|Type\s+|1024-blocks|\s+Used|\s+Available|\s+Capacity|\s+Mounted on\s*$/g;
const boundaries = [];
let match;
while ((match = regex.exec(header))) {
boundaries.push(match[0].length);
}
boundaries[boundaries.length - 1] = -1;
const data = lines.slice(1).map((line) => {
const cl = boundaries.map((boundary) => {
const column = boundary > 0 ? line.slice(0, boundary) : line;
line = line.slice(boundary);
return column.trim();
});
return {
capacity: Number.parseInt(cl[5], 10) / 100
};
});
if (data.length > 0) {
const { capacity } = data[0];
if (capacity > 0.8) {
lowDiskSpace = true;
}
}
} catch (error) {
console.log(error);
}
await cleanupDockerStorage(destination.id, lowDiskSpace, false)
}
await prisma.$disconnect();
} else process.exit(0);
})();

View File

@@ -4,151 +4,82 @@ import fs from 'fs/promises';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import { copyBaseConfigurationFiles, makeLabelForStandaloneApplication, saveBuildLog, setDefaultConfiguration } from '../lib/buildPacks/common'; import { copyBaseConfigurationFiles, makeLabelForStandaloneApplication, saveBuildLog, setDefaultConfiguration } from '../lib/buildPacks/common';
import { createDirectories, decrypt, executeDockerCmd, getDomain, prisma } from '../lib/common'; import { createDirectories, decrypt, defaultComposeConfiguration, executeDockerCmd, getDomain, prisma, decryptApplication } from '../lib/common';
import * as importers from '../lib/importers'; import * as importers from '../lib/importers';
import * as buildpacks from '../lib/buildPacks'; import * as buildpacks from '../lib/buildPacks';
(async () => { (async () => {
if (parentPort) { if (parentPort) {
const concurrency = 1
const PQueue = await import('p-queue');
const queue = new PQueue.default({ concurrency });
parentPort.on('message', async (message) => { parentPort.on('message', async (message) => {
if (parentPort) { if (message === 'error') throw new Error('oops');
if (message === 'error') throw new Error('oops'); if (message === 'cancel') {
if (message === 'cancel') { parentPort.postMessage('cancelled');
parentPort.postMessage('cancelled'); await prisma.$disconnect()
return; process.exit(0);
} }
if (message === 'status:autoUpdater') { });
parentPort.postMessage({ size: queue.size, pending: queue.pending, caller: 'autoUpdater' }); const pThrottle = await import('p-throttle')
return; const throttle = pThrottle.default({
} limit: 1,
if (message === 'status:cleanupStorage') { interval: 2000
parentPort.postMessage({ size: queue.size, pending: queue.pending, caller: 'cleanupStorage' }); });
return;
}
await queue.add(async () => {
const {
id: applicationId,
repository,
name,
destinationDocker,
destinationDockerId,
gitSource,
build_id: buildId,
configHash,
fqdn,
projectId,
secrets,
phpModules,
type,
pullmergeRequestId = null,
sourceBranch = null,
settings,
persistentStorage,
pythonWSGI,
pythonModule,
pythonVariable,
denoOptions,
exposePort,
baseImage,
baseBuildImage,
deploymentType,
} = message
let {
branch,
buildPack,
port,
installCommand,
buildCommand,
startCommand,
baseDirectory,
publishDirectory,
dockerFileLocation,
denoMainFile
} = message
try {
const { debug } = settings;
if (concurrency === 1) {
await prisma.build.updateMany({
where: {
status: { in: ['queued', 'running'] },
id: { not: buildId },
applicationId,
createdAt: { lt: new Date(new Date().getTime() - 10 * 1000) }
},
data: { status: 'failed' }
});
}
let imageId = applicationId;
let domain = getDomain(fqdn);
const 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
if (pullmergeRequestId) {
branch = sourceBranch;
domain = `${pullmergeRequestId}.${domain}`;
imageId = `${applicationId}-${pullmergeRequestId}`;
}
let deployNeeded = true; const th = throttle(async () => {
let destinationType; try {
const queuedBuilds = await prisma.build.findMany({ where: { status: { in: ['queued', 'running'] } }, orderBy: { createdAt: 'asc' } });
if (destinationDockerId) { const { concurrentBuilds } = await prisma.setting.findFirst({})
destinationType = 'docker'; if (queuedBuilds.length > 0) {
} parentPort.postMessage({ deploying: true });
if (destinationType === 'docker') { const concurrency = concurrentBuilds;
await prisma.build.update({ where: { id: buildId }, data: { status: 'running' } }); const pAll = await import('p-all');
const { workdir, repodir } = await createDirectories({ repository, buildId }); const actions = []
const configuration = await setDefaultConfiguration(message);
buildPack = configuration.buildPack;
port = configuration.port;
installCommand = configuration.installCommand;
startCommand = configuration.startCommand;
buildCommand = configuration.buildCommand;
publishDirectory = configuration.publishDirectory;
baseDirectory = configuration.baseDirectory;
dockerFileLocation = configuration.dockerFileLocation;
denoMainFile = configuration.denoMainFile;
const commit = await importers[gitSource.type]({
applicationId,
debug,
workdir,
repodir,
githubAppId: gitSource.githubApp?.id,
gitlabAppId: gitSource.gitlabApp?.id,
customPort: gitSource.customPort,
repository,
branch,
buildId,
apiUrl: gitSource.apiUrl,
htmlUrl: gitSource.htmlUrl,
projectId,
deployKeyId: gitSource.gitlabApp?.deployKeyId || null,
privateSshKey: decrypt(gitSource.gitlabApp?.privateSshKey) || null
});
if (!commit) {
throw new Error('No commit found?');
}
let tag = commit.slice(0, 7);
if (pullmergeRequestId) {
tag = `${commit.slice(0, 7)}-${pullmergeRequestId}`;
}
for (const queueBuild of queuedBuilds) {
actions.push(async () => {
let application = await prisma.application.findUnique({ where: { id: queueBuild.applicationId }, include: { destinationDocker: true, gitSource: { include: { githubApp: true, gitlabApp: true } }, persistentStorage: true, secrets: true, settings: true, teams: true } })
let { id: buildId, type, sourceBranch = null, pullmergeRequestId = null, forceRebuild } = queueBuild
application = decryptApplication(application)
try { try {
await prisma.build.update({ where: { id: buildId }, data: { commit } }); if (queueBuild.status === 'running') {
} catch (err) { await saveBuildLog({ line: 'Building halted, restarting...', buildId, applicationId: application.id });
console.log(err); }
} const {
if (!pullmergeRequestId) { id: applicationId,
repository,
name,
destinationDocker,
destinationDockerId,
gitSource,
configHash,
fqdn,
projectId,
secrets,
phpModules,
settings,
persistentStorage,
pythonWSGI,
pythonModule,
pythonVariable,
denoOptions,
exposePort,
baseImage,
baseBuildImage,
deploymentType,
} = application
let {
branch,
buildPack,
port,
installCommand,
buildCommand,
startCommand,
baseDirectory,
publishDirectory,
dockerFileLocation,
denoMainFile
} = application
const currentHash = crypto const currentHash = crypto
//@ts-ignore
.createHash('sha256') .createHash('sha256')
.update( .update(
JSON.stringify({ JSON.stringify({
@@ -156,7 +87,7 @@ import * as buildpacks from '../lib/buildPacks';
pythonModule, pythonModule,
pythonVariable, pythonVariable,
deploymentType, deploymentType,
denoOptions, denoOptions,
baseImage, baseImage,
baseBuildImage, baseBuildImage,
buildPack, buildPack,
@@ -172,42 +103,180 @@ import * as buildpacks from '../lib/buildPacks';
}) })
) )
.digest('hex'); .digest('hex');
const { debug } = settings;
if (configHash !== currentHash) { if (concurrency === 1) {
await prisma.application.update({ await prisma.build.updateMany({
where: { id: applicationId }, where: {
data: { configHash: currentHash } status: { in: ['queued', 'running'] },
id: { not: buildId },
applicationId,
createdAt: { lt: new Date(new Date().getTime() - 10 * 1000) }
},
data: { status: 'failed' }
}); });
deployNeeded = true;
if (configHash) {
await saveBuildLog({ line: 'Configuration changed.', buildId, applicationId });
}
} else {
deployNeeded = false;
} }
} else { let imageId = applicationId;
deployNeeded = true; let domain = getDomain(fqdn);
} const 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
if (pullmergeRequestId) {
branch = sourceBranch;
domain = `${pullmergeRequestId}.${domain}`;
imageId = `${applicationId}-${pullmergeRequestId}`;
}
let imageFound = false; let deployNeeded = true;
try { let destinationType;
await executeDockerCmd({
dockerId: destinationDocker.id, if (destinationDockerId) {
command: `docker image inspect ${applicationId}:${tag}` destinationType = 'docker';
}) }
imageFound = true; if (destinationType === 'docker') {
} catch (error) { await prisma.build.update({ where: { id: buildId }, data: { status: 'running' } });
// const { workdir, repodir } = await createDirectories({ repository, buildId });
} const configuration = await setDefaultConfiguration(application);
await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId, baseImage);
if (!imageFound || deployNeeded) { buildPack = configuration.buildPack;
// if (true) { port = configuration.port;
if (buildpacks[buildPack]) installCommand = configuration.installCommand;
await buildpacks[buildPack]({ startCommand = configuration.startCommand;
dockerId: destinationDocker.id, buildCommand = configuration.buildCommand;
buildId, publishDirectory = configuration.publishDirectory;
baseDirectory = configuration.baseDirectory;
dockerFileLocation = configuration.dockerFileLocation;
denoMainFile = configuration.denoMainFile;
const commit = await importers[gitSource.type]({
applicationId, applicationId,
domain, debug,
workdir,
repodir,
githubAppId: gitSource.githubApp?.id,
gitlabAppId: gitSource.gitlabApp?.id,
customPort: gitSource.customPort,
repository,
branch,
buildId,
apiUrl: gitSource.apiUrl,
htmlUrl: gitSource.htmlUrl,
projectId,
deployKeyId: gitSource.gitlabApp?.deployKeyId || null,
privateSshKey: decrypt(gitSource.gitlabApp?.privateSshKey) || null,
forPublic: gitSource.forPublic
});
if (!commit) {
throw new Error('No commit found?');
}
let tag = commit.slice(0, 7);
if (pullmergeRequestId) {
tag = `${commit.slice(0, 7)}-${pullmergeRequestId}`;
}
try {
await prisma.build.update({ where: { id: buildId }, data: { commit } });
} catch (err) {
console.log(err);
}
if (!pullmergeRequestId) {
if (configHash !== currentHash) {
deployNeeded = true;
if (configHash) {
await saveBuildLog({ line: 'Configuration changed.', buildId, applicationId });
}
} else {
deployNeeded = false;
}
} else {
deployNeeded = true;
}
let imageFound = false;
try {
await executeDockerCmd({
dockerId: destinationDocker.id,
command: `docker image inspect ${applicationId}:${tag}`
})
imageFound = true;
} catch (error) {
//
}
await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId, baseImage);
if (forceRebuild) deployNeeded = true
if (!imageFound || deployNeeded) {
// if (true) {
if (buildpacks[buildPack])
await buildpacks[buildPack]({
dockerId: destinationDocker.id,
buildId,
applicationId,
domain,
name,
type,
pullmergeRequestId,
buildPack,
repository,
branch,
projectId,
publishDirectory,
debug,
commit,
tag,
workdir,
port: exposePort ? `${exposePort}:${port}` : port,
installCommand,
buildCommand,
startCommand,
baseDirectory,
secrets,
phpModules,
pythonWSGI,
pythonModule,
pythonVariable,
dockerFileLocation,
denoMainFile,
denoOptions,
baseImage,
baseBuildImage,
deploymentType
});
else {
await saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId });
throw new Error(`Build pack ${buildPack} not found.`);
}
} else {
await saveBuildLog({ line: 'Build image already available - no rebuild required.', buildId, applicationId });
}
try {
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker stop -t 0 ${imageId}` })
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker rm ${imageId}` })
} catch (error) {
//
}
const envs = [
`PORT=${port}`
];
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
envs.push(`${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
envs.push(`${secret.name}=${secret.value}`);
}
}
});
}
await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
const labels = makeLabelForStandaloneApplication({
applicationId,
fqdn,
name, name,
type, type,
pullmergeRequestId, pullmergeRequestId,
@@ -215,151 +284,96 @@ import * as buildpacks from '../lib/buildPacks';
repository, repository,
branch, branch,
projectId, projectId,
publishDirectory,
debug,
commit,
tag,
workdir,
port: exposePort ? `${exposePort}:${port}` : port, port: exposePort ? `${exposePort}:${port}` : port,
commit,
installCommand, installCommand,
buildCommand, buildCommand,
startCommand, startCommand,
baseDirectory, baseDirectory,
secrets, publishDirectory
phpModules,
pythonWSGI,
pythonModule,
pythonVariable,
dockerFileLocation,
denoMainFile,
denoOptions,
baseImage,
baseBuildImage,
deploymentType
}); });
else { let envFound = false;
await saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId }); try {
throw new Error(`Build pack ${buildPack} not found.`); envFound = !!(await fs.stat(`${workdir}/.env`));
} } catch (error) {
} else { //
await saveBuildLog({ line: 'Build image already available - no rebuild required.', buildId, applicationId });
}
try {
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker stop -t 0 ${imageId}` })
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker rm ${imageId}` })
} catch (error) {
//
}
const envs = [];
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
envs.push(`${secret.name}='${secret.value}'`);
}
} else {
if (!secret.isPRMRSecret) {
envs.push(`${secret.name}='${secret.value}'`);
}
} }
}); try {
} await saveBuildLog({ line: 'Deployment started.', buildId, applicationId });
await fs.writeFile(`${workdir}/.env`, envs.join('\n')); const composeVolumes = volumes.map((volume) => {
const labels = makeLabelForStandaloneApplication({ return {
applicationId, [`${volume.split(':')[0]}`]: {
fqdn, name: volume.split(':')[0]
name,
type,
pullmergeRequestId,
buildPack,
repository,
branch,
projectId,
port: exposePort ? `${exposePort}:${port}` : port,
commit,
installCommand,
buildCommand,
startCommand,
baseDirectory,
publishDirectory
});
let envFound = false;
try {
envFound = !!(await fs.stat(`${workdir}/.env`));
} catch (error) {
//
}
try {
await saveBuildLog({ line: 'Deployment started.', buildId, applicationId });
const composeVolumes = volumes.map((volume) => {
return {
[`${volume.split(':')[0]}`]: {
name: volume.split(':')[0]
}
};
});
const composeFile = {
version: '3.8',
services: {
[imageId]: {
image: `${applicationId}:${tag}`,
container_name: imageId,
volumes,
env_file: envFound ? [`${workdir}/.env`] : [],
networks: [destinationDocker.network],
labels,
depends_on: [],
restart: 'always',
expose: [port],
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
// logging: {
// driver: 'fluentd',
// },
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
} }
} };
} });
}, const composeFile = {
networks: { version: '3.8',
[destinationDocker.network]: { services: {
external: true [imageId]: {
} image: `${applicationId}:${tag}`,
}, container_name: imageId,
volumes: Object.assign({}, ...composeVolumes) volumes,
}; env_file: envFound ? [`${workdir}/.env`] : [],
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile)); labels,
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` }) depends_on: [],
await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId }); expose: [port],
} catch (error) { ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
await saveBuildLog({ line: error, buildId, applicationId }); // logging: {
await prisma.build.update({ // driver: 'fluentd',
where: { id: message.build_id }, // },
...defaultComposeConfiguration(destinationDocker.network),
}
},
networks: {
[destinationDocker.network]: {
external: true
}
},
volumes: Object.assign({}, ...composeVolumes)
};
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` })
await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
} catch (error) {
await saveBuildLog({ line: error, buildId, applicationId });
await prisma.build.updateMany({
where: { id: buildId, status: { in: ['queued', 'running'] } },
data: { status: 'failed' }
});
throw new Error(error);
}
await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId });
await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } });
if (!pullmergeRequestId) await prisma.application.update({
where: { id: applicationId },
data: { configHash: currentHash }
});
}
}
catch (error) {
await prisma.build.updateMany({
where: { id: buildId, status: { in: ['queued', 'running'] } },
data: { status: 'failed' } data: { status: 'failed' }
}); });
throw new Error(error); await saveBuildLog({ line: error, buildId, applicationId: application.id });
} }
await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId }); });
await prisma.build.update({ where: { id: message.build_id }, data: { status: 'success' } });
}
} }
catch (error) {
await prisma.build.update({ await pAll.default(actions, { concurrency })
where: { id: message.build_id }, }
data: { status: 'failed' } } catch (error) {
}); console.log(error)
await saveBuildLog({ line: error, buildId, applicationId }); } finally {
} finally {
await prisma.$disconnect();
}
});
await prisma.$disconnect();
} }
}); })
while (true) {
await th()
}
} else process.exit(0); } else process.exit(0);
})(); })();

View File

@@ -0,0 +1,216 @@
import { parentPort } from 'node:worker_threads';
import axios from 'axios';
import compareVersions from 'compare-versions';
import { asyncExecShell, cleanupDockerStorage, executeDockerCmd, isDev, prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, listSettings, version } from '../lib/common';
async function disconnect() {
await prisma.$disconnect();
}
async function autoUpdater() {
try {
const currentVersion = version;
const { data: versions } = await axios
.get(
`https://get.coollabs.io/versions.json`
, {
params: {
appId: process.env['COOLIFY_APP_ID'] || undefined,
version: currentVersion
}
})
const latestVersion = versions['coolify'].main.version;
const isUpdateAvailable = compareVersions(latestVersion, currentVersion);
if (isUpdateAvailable === 1) {
const activeCount = 0
if (activeCount === 0) {
if (!isDev) {
console.log(`Updating Coolify to ${latestVersion}.`);
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
await asyncExecShell(`env | grep COOLIFY > .env`);
await asyncExecShell(
`docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify && docker rm coolify && docker compose up -d --force-recreate"`
);
} else {
console.log('Updating (not really in dev mode).');
}
}
}
} catch (error) {
console.log(error);
}
}
async function checkProxies() {
try {
const { default: isReachable } = await import('is-port-reachable');
let portReachable;
const { arch, ipv4, ipv6 } = await listSettings();
// Coolify Proxy local
const engine = '/var/run/docker.sock';
const localDocker = await prisma.destinationDocker.findFirst({
where: { engine, network: 'coolify' }
});
if (localDocker && localDocker.isCoolifyProxyUsed) {
portReachable = await isReachable(80, { host: ipv4 || ipv6 })
if (!portReachable) {
await startTraefikProxy(localDocker.id);
}
}
// TCP Proxies
const databasesWithPublicPort = await prisma.database.findMany({
where: { publicPort: { not: null } },
include: { settings: true, destinationDocker: true }
});
for (const database of databasesWithPublicPort) {
const { destinationDockerId, destinationDocker, publicPort, id } = database;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
const { privatePort } = generateDatabaseConfiguration(database, arch);
portReachable = await isReachable(publicPort, { host: destinationDocker.remoteIpAddress || ipv4 || ipv6 })
if (!portReachable) {
await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
}
}
}
const wordpressWithFtp = await prisma.wordpress.findMany({
where: { ftpPublicPort: { not: null } },
include: { service: { include: { destinationDocker: true } } }
});
for (const ftp of wordpressWithFtp) {
const { service, ftpPublicPort } = ftp;
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
portReachable = await isReachable(ftpPublicPort, { host: destinationDocker.remoteIpAddress || ipv4 || ipv6 })
if (!portReachable) {
await startTraefikTCPProxy(destinationDocker, id, ftpPublicPort, 22, 'wordpressftp');
}
}
}
// HTTP Proxies
const minioInstances = await prisma.minio.findMany({
where: { publicPort: { not: null } },
include: { service: { include: { destinationDocker: true } } }
});
for (const minio of minioInstances) {
const { service, publicPort } = minio;
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
portReachable = await isReachable(publicPort, { host: destinationDocker.remoteIpAddress || ipv4 || ipv6 })
if (!portReachable) {
await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000);
}
}
}
} catch (error) {
}
}
async function cleanupPrismaEngines() {
if (!isDev) {
try {
const { stdout } = await asyncExecShell(`ps -ef | grep /app/prisma-engines/query-engine | grep -v grep | wc -l | xargs`)
if (stdout.trim() != null && stdout.trim() != '' && Number(stdout.trim()) > 1) {
await asyncExecShell(`killall -q -e /app/prisma-engines/query-engine -o 1m`)
}
} catch (error) {
console.log(error);
}
}
}
async function cleanupStorage() {
const destinationDockers = await prisma.destinationDocker.findMany();
let enginesDone = new Set()
for (const destination of destinationDockers) {
if (enginesDone.has(destination.engine) || enginesDone.has(destination.remoteIpAddress)) return
if (destination.engine) enginesDone.add(destination.engine)
if (destination.remoteIpAddress) enginesDone.add(destination.remoteIpAddress)
let lowDiskSpace = false;
try {
let stdout = null
if (!isDev) {
const output = await executeDockerCmd({ dockerId: destination.id, command: `CONTAINER=$(docker ps -lq | head -1) && docker exec $CONTAINER sh -c 'df -kPT /'` })
stdout = output.stdout;
} else {
const output = await asyncExecShell(
`df -kPT /`
);
stdout = output.stdout;
}
let lines = stdout.trim().split('\n');
let header = lines[0];
let regex =
/^Filesystem\s+|Type\s+|1024-blocks|\s+Used|\s+Available|\s+Capacity|\s+Mounted on\s*$/g;
const boundaries = [];
let match;
while ((match = regex.exec(header))) {
boundaries.push(match[0].length);
}
boundaries[boundaries.length - 1] = -1;
const data = lines.slice(1).map((line) => {
const cl = boundaries.map((boundary) => {
const column = boundary > 0 ? line.slice(0, boundary) : line;
line = line.slice(boundary);
return column.trim();
});
return {
capacity: Number.parseInt(cl[5], 10) / 100
};
});
if (data.length > 0) {
const { capacity } = data[0];
if (capacity > 0.8) {
lowDiskSpace = true;
}
}
} catch (error) {
console.log(error);
}
await cleanupDockerStorage(destination.id, lowDiskSpace, false)
}
}
(async () => {
let status = {
cleanupStorage: false,
autoUpdater: false
}
if (parentPort) {
parentPort.on('message', async (message) => {
if (parentPort) {
if (message === 'error') throw new Error('oops');
if (message === 'cancel') {
parentPort.postMessage('cancelled');
process.exit(1);
}
if (message === 'action:cleanupStorage') {
if (!status.autoUpdater) {
status.cleanupStorage = true
await cleanupStorage();
status.cleanupStorage = false
}
return;
}
if (message === 'action:cleanupPrismaEngines') {
await cleanupPrismaEngines();
return;
}
if (message === 'action:checkProxies') {
await checkProxies();
return;
}
if (message === 'action:autoUpdater') {
if (!status.cleanupStorage) {
status.autoUpdater = true
await autoUpdater();
status.autoUpdater = false
}
return;
}
}
});
} else process.exit(0);
})();

View File

@@ -541,9 +541,6 @@ export async function buildImage({
} else { } else {
await saveBuildLog({ line: `Building image started.`, buildId, applicationId }); await saveBuildLog({ line: `Building image started.`, buildId, applicationId });
} }
if (debug) {
await saveBuildLog({ line: `\n###############\nIMPORTANT: Due to some issues during implementing Remote Docker Engine, the builds logs are not streamed at the moment - but will be soon! You will see the full build log when the build is finished!\n###############`, buildId, applicationId });
}
if (!debug && isCache) { if (!debug && isCache) {
await saveBuildLog({ await saveBuildLog({
line: `Debug turned off. To see more details, allow it in the configuration.`, line: `Debug turned off. To see more details, allow it in the configuration.`,
@@ -553,54 +550,11 @@ export async function buildImage({
} }
const dockerFile = isCache ? `${dockerFileLocation}-cache` : `${dockerFileLocation}` const dockerFile = isCache ? `${dockerFileLocation}-cache` : `${dockerFileLocation}`
const cache = `${applicationId}:${tag}${isCache ? '-cache' : ''}` const cache = `${applicationId}:${tag}${isCache ? '-cache' : ''}`
const { stderr } = await executeDockerCmd({ dockerId, command: `docker build --progress plain -f ${workdir}/${dockerFile} -t ${cache} ${workdir}` }) await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker build --progress plain -f ${workdir}/${dockerFile} -t ${cache} ${workdir}` })
if (debug) { const { status } = await prisma.build.findUnique({ where: { id: buildId } })
const array = stderr.split('\n') if (status === 'canceled') {
for (const line of array) { throw new Error('Deployment canceled.')
if (line !== '\n') {
await saveBuildLog({
line: `${line.replace('\n', '')}`,
buildId,
applicationId
});
}
}
} }
// await new Promise((resolve, reject) => {
// const command = spawn(`docker`, ['build', '-f', `${workdir}${dockerFile}`, '-t', `${cache}`,`${workdir}`], {
// env: {
// DOCKER_HOST: 'ssh://root@95.217.178.202',
// DOCKER_BUILDKIT: '1'
// }
// });
// command.stdout.on('data', function (data) {
// console.log('stdout: ' + data);
// });
// command.stderr.on('data', function (data) {
// console.log('stderr: ' + data);
// });
// command.on('error', function (error) {
// console.log(error)
// reject(error)
// })
// command.on('exit', function (code) {
// console.log('exit code: ' + code);
// resolve(code)
// });
// })
// console.log({ stdout, stderr })
// const stream = await docker.engine.buildImage(
// { src: ['.'], context: workdir },
// {
// dockerfile: isCache ? `${dockerFileLocation}-cache` : dockerFileLocation,
// t: `${applicationId}:${tag}${isCache ? '-cache' : ''}`
// }
// );
// await streamEvents({ stream, docker, buildId, applicationId, debug });
if (isCache) { if (isCache) {
await saveBuildLog({ line: `Building cache image successful.`, buildId, applicationId }); await saveBuildLog({ line: `Building cache image successful.`, buildId, applicationId });
} else { } else {
@@ -717,11 +671,10 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
if (isPnpm) { if (isPnpm) {
Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7'); Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7');
} }
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
if (installCommand) { if (installCommand) {
Dockerfile.push(`COPY .${baseDirectory || ''}/package.json ./`);
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({ ...data, isCache: true }); await buildImage({ ...data, isCache: true });

View File

@@ -1,4 +1,4 @@
import child from 'child_process'; import { exec } from 'node:child_process'
import util from 'util'; import util from 'util';
import fs from 'fs/promises'; import fs from 'fs/promises';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
@@ -16,8 +16,10 @@ import sshConfig from 'ssh-config'
import { checkContainer, removeContainer } from './docker'; import { checkContainer, removeContainer } from './docker';
import { day } from './dayjs'; import { day } from './dayjs';
import * as serviceFields from './serviceFields' import * as serviceFields from './serviceFields'
import { saveBuildLog } from './buildPacks/common';
import { scheduler } from './scheduler';
export const version = '3.5.0'; export const version = '3.8.5';
export const isDev = process.env.NODE_ENV === 'development'; export const isDev = process.env.NODE_ENV === 'development';
const algorithm = 'aes-256-ctr'; const algorithm = 'aes-256-ctr';
@@ -79,17 +81,86 @@ export const include: any = {
hasura: true, hasura: true,
fider: true, fider: true,
moodle: true, moodle: true,
appwrite: true appwrite: true,
glitchTip: true,
searxng: true
}; };
export const uniqueName = (): string => uniqueNamesGenerator(customConfig); export const uniqueName = (): string => uniqueNamesGenerator(customConfig);
export const asyncExecShell = util.promisify(child.exec); export const asyncExecShell = util.promisify(exec);
export const asyncExecShellStream = async ({ debug, buildId, applicationId, command, engine }: { debug: boolean, buildId: string, applicationId: string, command: string, engine: string }) => {
return await new Promise(async (resolve, reject) => {
const { execaCommand } = await import('execa')
const subprocess = execaCommand(command, { env: { DOCKER_BUILDKIT: "1", DOCKER_HOST: engine } })
if (debug) {
subprocess.stdout.on('data', async (data) => {
const stdout = data.toString();
const array = stdout.split('\n')
for (const line of array) {
if (line !== '\n' && line !== '') {
await saveBuildLog({
line: `${line.replace('\n', '')}`,
buildId,
applicationId
});
}
}
})
subprocess.stderr.on('data', async (data) => {
const stderr = data.toString();
const array = stderr.split('\n')
for (const line of array) {
if (line !== '\n' && line !== '') {
await saveBuildLog({
line: `${line.replace('\n', '')}`,
buildId,
applicationId
});
}
}
})
}
subprocess.on('exit', async (code) => {
await asyncSleep(1000);
if (code === 0) {
resolve(code)
} else {
reject(code)
}
})
})
}
export const asyncSleep = (delay: number): Promise<unknown> => export const asyncSleep = (delay: number): Promise<unknown> =>
new Promise((resolve) => setTimeout(resolve, delay)); new Promise((resolve) => setTimeout(resolve, delay));
export const prisma = new PrismaClient({ export const prisma = new PrismaClient({
errorFormat: 'minimal' errorFormat: 'minimal',
// log: [
// {
// emit: 'event',
// level: 'query',
// },
// {
// emit: 'stdout',
// level: 'error',
// },
// {
// emit: 'stdout',
// level: 'info',
// },
// {
// emit: 'stdout',
// level: 'warn',
// },
// ],
}); });
// prisma.$on('query', (e) => {
// console.log({e})
// console.log('Query: ' + e.query)
// console.log('Params: ' + e.params)
// console.log('Duration: ' + e.duration + 'ms')
// })
export const base64Encode = (text: string): string => { export const base64Encode = (text: string): string => {
return Buffer.from(text).toString('base64'); return Buffer.from(text).toString('base64');
}; };
@@ -287,7 +358,7 @@ export const supportedServiceTypesAndVersions = [
ports: { ports: {
main: 80 main: 80
} }
} },
// { // {
// name: 'moodle', // name: 'moodle',
// fancyName: 'Moodle', // fancyName: 'Moodle',
@@ -299,6 +370,28 @@ export const supportedServiceTypesAndVersions = [
// main: 8080 // main: 8080
// } // }
// } // }
{
name: 'glitchTip',
fancyName: 'GlitchTip',
baseImage: 'glitchtip/glitchtip',
images: ['postgres:14-alpine', 'redis:7-alpine'],
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 8000
}
},
{
name: 'searxng',
fancyName: 'SearXNG',
baseImage: 'searxng/searxng',
images: [],
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 8080
}
},
]; ];
export async function checkDoubleBranch(branch: string, projectId: number): Promise<boolean> { export async function checkDoubleBranch(branch: string, projectId: number): Promise<boolean> {
@@ -533,21 +626,38 @@ export const supportedDatabaseTypesAndVersions = [
} }
]; ];
export async function getFreeSSHLocalPort(id: string): Promise<number> { export async function getFreeSSHLocalPort(id: string): Promise<number | boolean> {
const { default: getPort, portNumbers } = await import('get-port'); const { default: isReachable } = await import('is-port-reachable');
const { remoteIpAddress, sshLocalPort } = await prisma.destinationDocker.findUnique({ where: { id } }) const { remoteIpAddress, sshLocalPort } = await prisma.destinationDocker.findUnique({ where: { id } })
if (sshLocalPort) { if (sshLocalPort) {
return Number(sshLocalPort) return Number(sshLocalPort)
} }
const data = await prisma.setting.findFirst();
const { minPort, maxPort } = data;
const ports = await prisma.destinationDocker.findMany({ where: { sshLocalPort: { not: null }, remoteIpAddress: { not: remoteIpAddress } } }) const ports = await prisma.destinationDocker.findMany({ where: { sshLocalPort: { not: null }, remoteIpAddress: { not: remoteIpAddress } } })
const alreadyConfigured = await prisma.destinationDocker.findFirst({ where: { remoteIpAddress, id: { not: id }, sshLocalPort: { not: null } } })
const alreadyConfigured = await prisma.destinationDocker.findFirst({
where: {
remoteIpAddress, id: { not: id }, sshLocalPort: { not: null }
}
})
if (alreadyConfigured?.sshLocalPort) { if (alreadyConfigured?.sshLocalPort) {
await prisma.destinationDocker.update({ where: { id }, data: { sshLocalPort: alreadyConfigured.sshLocalPort } }) await prisma.destinationDocker.update({ where: { id }, data: { sshLocalPort: alreadyConfigured.sshLocalPort } })
return Number(alreadyConfigured.sshLocalPort) return Number(alreadyConfigured.sshLocalPort)
} }
const availablePort = await getPort({ port: portNumbers(10000, 10100), exclude: ports.map(p => p.sshLocalPort) }) const range = generateRangeArray(minPort, maxPort)
await prisma.destinationDocker.update({ where: { id }, data: { sshLocalPort: Number(availablePort) } }) console.log({ ports })
return Number(availablePort) const availablePorts = range.filter(port => !ports.map(p => p.sshLocalPort).includes(port))
for (const port of availablePorts) {
const found = await isReachable(port, { host: 'localhost' })
if (!found) {
await prisma.destinationDocker.update({ where: { id }, data: { sshLocalPort: Number(port) } })
return Number(port)
}
}
return false
} }
export async function createRemoteEngineConfiguration(id: string) { export async function createRemoteEngineConfiguration(id: string) {
@@ -579,7 +689,7 @@ export async function createRemoteEngineConfiguration(id: string) {
config.append({ config.append({
Host: remoteIpAddress, Host: remoteIpAddress,
Hostname: 'localhost', Hostname: 'localhost',
Port: Number(localPort), Port: localPort.toString(),
User: remoteUser, User: remoteUser,
IdentityFile: sshKeyFile, IdentityFile: sshKeyFile,
StrictHostKeyChecking: 'no' StrictHostKeyChecking: 'no'
@@ -592,7 +702,7 @@ export async function createRemoteEngineConfiguration(id: string) {
} }
return await fs.writeFile(`${homedir}/.ssh/config`, sshConfig.stringify(config)) return await fs.writeFile(`${homedir}/.ssh/config`, sshConfig.stringify(config))
} }
export async function executeDockerCmd({ dockerId, command }: { dockerId: string, command: string }) { export async function executeDockerCmd({ debug, buildId, applicationId, dockerId, command }: { debug?: boolean, buildId?: string, applicationId?: string, dockerId: string, command: string }): Promise<any> {
let { remoteEngine, remoteIpAddress, engine } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } }) let { remoteEngine, remoteIpAddress, engine } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } })
if (remoteEngine) { if (remoteEngine) {
await createRemoteEngineConfiguration(dockerId) await createRemoteEngineConfiguration(dockerId)
@@ -605,6 +715,9 @@ export async function executeDockerCmd({ dockerId, command }: { dockerId: string
command = command.replace(/docker compose/gi, 'docker-compose') command = command.replace(/docker compose/gi, 'docker-compose')
} }
} }
if (command.startsWith(`docker build --progress plain`)) {
return await asyncExecShellStream({ debug, buildId, applicationId, command, engine });
}
return await asyncExecShell( return await asyncExecShell(
`DOCKER_BUILDKIT=1 DOCKER_HOST="${engine}" ${command}` `DOCKER_BUILDKIT=1 DOCKER_HOST="${engine}" ${command}`
); );
@@ -724,13 +837,18 @@ export async function listSettings(): Promise<any> {
} }
export function generatePassword(length = 24, symbols = false): string { export function generatePassword({ length = 24, symbols = false, isHex = false }: { length?: number, symbols?: boolean, isHex?: boolean } | null): string {
return generator.generate({ if (isHex) {
return crypto.randomBytes(length).toString("hex");
}
const password = generator.generate({
length, length,
numbers: true, numbers: true,
strict: true, strict: true,
symbols symbols
}); });
return password;
} }
export function generateDatabaseConfiguration(database: any, arch: string): export function generateDatabaseConfiguration(database: any, arch: string):
@@ -1176,8 +1294,27 @@ export async function updatePasswordInDb(database, user, newPassword, isRoot) {
} }
} }
} }
export async function checkExposedPort({ id, configuredPort, exposePort, dockerId, remoteIpAddress }: { id: string, configuredPort?: number, exposePort: number, dockerId: string, remoteIpAddress?: string }) {
if (exposePort < 1024 || exposePort > 65535) {
throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` }
}
if (configuredPort) {
if (configuredPort !== exposePort) {
const availablePort = await getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress);
if (availablePort.toString() !== exposePort.toString()) {
throw { status: 500, message: `Port ${exposePort} is already in use.` }
}
}
} else {
const availablePort = await getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress);
if (availablePort.toString() !== exposePort.toString()) {
throw { status: 500, message: `Port ${exposePort} is already in use.` }
}
}
}
export async function getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress) { export async function getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress) {
const { default: getPort } = await import('get-port'); const { default: checkPort } = await import('is-port-reachable');
const applicationUsed = await ( const applicationUsed = await (
await prisma.application.findMany({ await prisma.application.findMany({
where: { exposePort: { not: null }, id: { not: id }, destinationDockerId: dockerId }, where: { exposePort: { not: null }, id: { not: id }, destinationDockerId: dockerId },
@@ -1191,22 +1328,23 @@ export async function getFreeExposedPort(id, exposePort, dockerId, remoteIpAddre
}) })
).map((a) => a.exposePort); ).map((a) => a.exposePort);
const usedPorts = [...applicationUsed, ...serviceUsed]; const usedPorts = [...applicationUsed, ...serviceUsed];
if (remoteIpAddress) { if (usedPorts.includes(exposePort)) {
const { default: checkPort } = await import('is-port-reachable');
const found = await checkPort(exposePort, { host: remoteIpAddress });
if (!found) {
return exposePort
}
return false return false
} }
return await getPort({ port: Number(exposePort), exclude: usedPorts }); const found = await checkPort(exposePort, { host: remoteIpAddress || 'localhost' });
if (!found) {
return exposePort
}
return false
} }
export function generateRangeArray(start, end) {
return Array.from({ length: (end - start) }, (v, k) => k + start);
}
export async function getFreePublicPort(id, dockerId) { export async function getFreePublicPort(id, dockerId) {
const { default: getPort, portNumbers } = await import('get-port'); const { default: isReachable } = await import('is-port-reachable');
const data = await prisma.setting.findFirst(); const data = await prisma.setting.findFirst();
const { minPort, maxPort } = data; const { minPort, maxPort } = data;
const dbUsed = await ( const dbUsed = await (
await prisma.database.findMany({ await prisma.database.findMany({
where: { publicPort: { not: null }, id: { not: id }, destinationDockerId: dockerId }, where: { publicPort: { not: null }, id: { not: id }, destinationDockerId: dockerId },
@@ -1232,7 +1370,15 @@ export async function getFreePublicPort(id, dockerId) {
}) })
).map((a) => a.publicPort); ).map((a) => a.publicPort);
const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed, ...minioUsed]; const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed, ...minioUsed];
return await getPort({ port: portNumbers(minPort, maxPort), exclude: usedPorts }); const range = generateRangeArray(minPort, maxPort)
const availablePorts = range.filter(port => !usedPorts.includes(port))
for (const port of availablePorts) {
const found = await isReachable(port, { host: 'localhost' })
if (!found) {
return port
}
}
return false
} }
export async function startTraefikTCPProxy( export async function startTraefikTCPProxy(
@@ -1361,11 +1507,11 @@ export async function configureServiceType({
type: string; type: string;
}): Promise<void> { }): Promise<void> {
if (type === 'plausibleanalytics') { if (type === 'plausibleanalytics') {
const password = encrypt(generatePassword()); const password = encrypt(generatePassword({}));
const postgresqlUser = cuid(); const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword()); const postgresqlPassword = encrypt(generatePassword({}));
const postgresqlDatabase = 'plausibleanalytics'; const postgresqlDatabase = 'plausibleanalytics';
const secretKeyBase = encrypt(generatePassword(64)); const secretKeyBase = encrypt(generatePassword({ length: 64 }));
await prisma.service.update({ await prisma.service.update({
where: { id }, where: { id },
@@ -1389,22 +1535,22 @@ export async function configureServiceType({
}); });
} else if (type === 'minio') { } else if (type === 'minio') {
const rootUser = cuid(); const rootUser = cuid();
const rootUserPassword = encrypt(generatePassword()); const rootUserPassword = encrypt(generatePassword({}));
await prisma.service.update({ await prisma.service.update({
where: { id }, where: { id },
data: { type, minio: { create: { rootUser, rootUserPassword } } } data: { type, minio: { create: { rootUser, rootUserPassword } } }
}); });
} else if (type === 'vscodeserver') { } else if (type === 'vscodeserver') {
const password = encrypt(generatePassword()); const password = encrypt(generatePassword({}));
await prisma.service.update({ await prisma.service.update({
where: { id }, where: { id },
data: { type, vscodeserver: { create: { password } } } data: { type, vscodeserver: { create: { password } } }
}); });
} else if (type === 'wordpress') { } else if (type === 'wordpress') {
const mysqlUser = cuid(); const mysqlUser = cuid();
const mysqlPassword = encrypt(generatePassword()); const mysqlPassword = encrypt(generatePassword({}));
const mysqlRootUser = cuid(); const mysqlRootUser = cuid();
const mysqlRootUserPassword = encrypt(generatePassword()); const mysqlRootUserPassword = encrypt(generatePassword({}));
await prisma.service.update({ await prisma.service.update({
where: { id }, where: { id },
data: { data: {
@@ -1442,11 +1588,11 @@ export async function configureServiceType({
}); });
} else if (type === 'ghost') { } else if (type === 'ghost') {
const defaultEmail = `${cuid()}@example.com`; const defaultEmail = `${cuid()}@example.com`;
const defaultPassword = encrypt(generatePassword()); const defaultPassword = encrypt(generatePassword({}));
const mariadbUser = cuid(); const mariadbUser = cuid();
const mariadbPassword = encrypt(generatePassword()); const mariadbPassword = encrypt(generatePassword({}));
const mariadbRootUser = cuid(); const mariadbRootUser = cuid();
const mariadbRootUserPassword = encrypt(generatePassword()); const mariadbRootUserPassword = encrypt(generatePassword({}));
await prisma.service.update({ await prisma.service.update({
where: { id }, where: { id },
@@ -1465,7 +1611,7 @@ export async function configureServiceType({
} }
}); });
} else if (type === 'meilisearch') { } else if (type === 'meilisearch') {
const masterKey = encrypt(generatePassword(32)); const masterKey = encrypt(generatePassword({ length: 32 }));
await prisma.service.update({ await prisma.service.update({
where: { id }, where: { id },
data: { data: {
@@ -1474,11 +1620,11 @@ export async function configureServiceType({
} }
}); });
} else if (type === 'umami') { } else if (type === 'umami') {
const umamiAdminPassword = encrypt(generatePassword()); const umamiAdminPassword = encrypt(generatePassword({}));
const postgresqlUser = cuid(); const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword()); const postgresqlPassword = encrypt(generatePassword({}));
const postgresqlDatabase = 'umami'; const postgresqlDatabase = 'umami';
const hashSalt = encrypt(generatePassword(64)); const hashSalt = encrypt(generatePassword({ length: 64 }));
await prisma.service.update({ await prisma.service.update({
where: { id }, where: { id },
data: { data: {
@@ -1496,9 +1642,9 @@ export async function configureServiceType({
}); });
} else if (type === 'hasura') { } else if (type === 'hasura') {
const postgresqlUser = cuid(); const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword()); const postgresqlPassword = encrypt(generatePassword({}));
const postgresqlDatabase = 'hasura'; const postgresqlDatabase = 'hasura';
const graphQLAdminPassword = encrypt(generatePassword()); const graphQLAdminPassword = encrypt(generatePassword({}));
await prisma.service.update({ await prisma.service.update({
where: { id }, where: { id },
data: { data: {
@@ -1515,9 +1661,9 @@ export async function configureServiceType({
}); });
} else if (type === 'fider') { } else if (type === 'fider') {
const postgresqlUser = cuid(); const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword()); const postgresqlPassword = encrypt(generatePassword({}));
const postgresqlDatabase = 'fider'; const postgresqlDatabase = 'fider';
const jwtSecret = encrypt(generatePassword(64, true)); const jwtSecret = encrypt(generatePassword({ length: 64, symbols: true }));
await prisma.service.update({ await prisma.service.update({
where: { id }, where: { id },
data: { data: {
@@ -1534,13 +1680,13 @@ export async function configureServiceType({
}); });
} else if (type === 'moodle') { } else if (type === 'moodle') {
const defaultUsername = cuid(); const defaultUsername = cuid();
const defaultPassword = encrypt(generatePassword()); const defaultPassword = encrypt(generatePassword({}));
const defaultEmail = `${cuid()} @example.com`; const defaultEmail = `${cuid()} @example.com`;
const mariadbUser = cuid(); const mariadbUser = cuid();
const mariadbPassword = encrypt(generatePassword()); const mariadbPassword = encrypt(generatePassword({}));
const mariadbDatabase = 'moodle_db'; const mariadbDatabase = 'moodle_db';
const mariadbRootUser = cuid(); const mariadbRootUser = cuid();
const mariadbRootUserPassword = encrypt(generatePassword()); const mariadbRootUserPassword = encrypt(generatePassword({}));
await prisma.service.update({ await prisma.service.update({
where: { id }, where: { id },
data: { data: {
@@ -1560,15 +1706,15 @@ export async function configureServiceType({
} }
}); });
} else if (type === 'appwrite') { } else if (type === 'appwrite') {
const opensslKeyV1 = encrypt(generatePassword()); const opensslKeyV1 = encrypt(generatePassword({}));
const executorSecret = encrypt(generatePassword()); const executorSecret = encrypt(generatePassword({}));
const redisPassword = encrypt(generatePassword()); const redisPassword = encrypt(generatePassword({}));
const mariadbHost = `${id}-mariadb` const mariadbHost = `${id}-mariadb`
const mariadbUser = cuid(); const mariadbUser = cuid();
const mariadbPassword = encrypt(generatePassword()); const mariadbPassword = encrypt(generatePassword({}));
const mariadbDatabase = 'appwrite'; const mariadbDatabase = 'appwrite';
const mariadbRootUser = cuid(); const mariadbRootUser = cuid();
const mariadbRootUserPassword = encrypt(generatePassword()); const mariadbRootUserPassword = encrypt(generatePassword({}));
await prisma.service.update({ await prisma.service.update({
where: { id }, where: { id },
data: { data: {
@@ -1588,6 +1734,47 @@ export async function configureServiceType({
} }
} }
}); });
} else if (type === 'glitchTip') {
const defaultUsername = cuid();
const defaultEmail = `${defaultUsername}@example.com`;
const defaultPassword = encrypt(generatePassword({}));
const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword({}));
const postgresqlDatabase = 'glitchTip';
const secretKeyBase = encrypt(generatePassword({ length: 64 }));
await prisma.service.update({
where: { id },
data: {
type,
glitchTip: {
create: {
postgresqlDatabase,
postgresqlUser,
postgresqlPassword,
secretKeyBase,
defaultEmail,
defaultUsername,
defaultPassword,
}
}
}
});
} else if (type === 'searxng') {
const secretKey = encrypt(generatePassword({ length: 32, isHex: true }))
const redisPassword = encrypt(generatePassword({}));
await prisma.service.update({
where: { id },
data: {
type,
searxng: {
create: {
secretKey,
redisPassword,
}
}
}
});
} else { } else {
await prisma.service.update({ await prisma.service.update({
where: { id }, where: { id },
@@ -1610,8 +1797,10 @@ export async function removeService({ id }: { id: string }): Promise<void> {
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.glitchTip.deleteMany({ where: { serviceId: id } });
await prisma.moodle.deleteMany({ where: { serviceId: id } }); await prisma.moodle.deleteMany({ where: { serviceId: id } });
await prisma.appwrite.deleteMany({ where: { serviceId: id } }); await prisma.appwrite.deleteMany({ where: { serviceId: id } });
await prisma.searxng.deleteMany({ where: { serviceId: id } });
await prisma.service.delete({ where: { id } }); await prisma.service.delete({ where: { id } });
} }
@@ -1704,18 +1893,22 @@ export async function stopBuild(buildId, applicationId) {
let count = 0; let count = 0;
await new Promise<void>(async (resolve, reject) => { await new Promise<void>(async (resolve, reject) => {
const { destinationDockerId, status } = await prisma.build.findFirst({ where: { id: buildId } }); const { destinationDockerId, status } = await prisma.build.findFirst({ where: { id: buildId } });
const { engine, id: dockerId } = await prisma.destinationDocker.findFirst({ where: { id: destinationDockerId } }); const { id: dockerId } = await prisma.destinationDocker.findFirst({ where: { id: destinationDockerId } });
const interval = setInterval(async () => { const interval = setInterval(async () => {
try { try {
if (status === 'failed') { if (status === 'failed' || status === 'canceled') {
clearInterval(interval); clearInterval(interval);
return resolve(); return resolve();
} }
if (count > 100) { if (count > 15) {
clearInterval(interval); clearInterval(interval);
return reject(new Error('Build canceled')); if (scheduler.workers.has('deployApplication')) {
scheduler.workers.get('deployApplication').postMessage('cancel')
}
await cleanupDB(buildId, applicationId);
return reject(new Error('Deployment canceled.'));
} }
const { stdout: buildContainers } = await executeDockerCmd({ dockerId, command: `docker container ls--filter "label=coolify.buildId=${buildId}" --format '{{json .}}'` }) const { stdout: buildContainers } = await executeDockerCmd({ dockerId, command: `docker container ls --filter "label=coolify.buildId=${buildId}" --format '{{json .}}'` })
if (buildContainers) { if (buildContainers) {
const containersArray = buildContainers.trim().split('\n'); const containersArray = buildContainers.trim().split('\n');
for (const container of containersArray) { for (const container of containersArray) {
@@ -1723,8 +1916,11 @@ export async function stopBuild(buildId, applicationId) {
const id = containerObj.ID; const id = containerObj.ID;
if (!containerObj.Names.startsWith(`${applicationId} `)) { if (!containerObj.Names.startsWith(`${applicationId} `)) {
await removeContainer({ id, dockerId }); await removeContainer({ id, dockerId });
await cleanupDB(buildId);
clearInterval(interval); clearInterval(interval);
if (scheduler.workers.has('deployApplication')) {
scheduler.workers.get('deployApplication').postMessage('cancel')
}
await cleanupDB(buildId, applicationId);
return resolve(); return resolve();
} }
} }
@@ -1735,11 +1931,12 @@ export async function stopBuild(buildId, applicationId) {
}); });
} }
async function cleanupDB(buildId: string) { async function cleanupDB(buildId: string, applicationId: string) {
const data = await prisma.build.findUnique({ where: { id: buildId } }); const data = await prisma.build.findUnique({ where: { id: buildId } });
if (data?.status === 'queued' || data?.status === 'running') { if (data?.status === 'queued' || data?.status === 'running') {
await prisma.build.update({ where: { id: buildId }, data: { status: 'failed' } }); await prisma.build.update({ where: { id: buildId }, data: { status: 'canceled' } });
} }
await saveBuildLog({ line: 'Deployment canceled.', buildId, applicationId });
} }
export function convertTolOldVolumeNames(type) { export function convertTolOldVolumeNames(type) {
@@ -1747,10 +1944,7 @@ export function convertTolOldVolumeNames(type) {
return 'nc' return 'nc'
} }
} }
// export async function getAvailableServices(): Promise<any> {
// const { data } = await axios.get(`https://gist.githubusercontent.com/andrasbacsai/4aac36d8d6214dbfc34fa78110554a50/raw/5b27e6c37d78aaeedc1148d797112c827a2f43cf/availableServices.json`)
// return data
//
export async function cleanupDockerStorage(dockerId, lowDiskSpace, force) { export async function cleanupDockerStorage(dockerId, lowDiskSpace, force) {
// Cleanup old coolify images // Cleanup old coolify images
try { try {
@@ -1783,6 +1977,12 @@ export async function cleanupDockerStorage(dockerId, lowDiskSpace, force) {
} catch (error) { } catch (error) {
//console.log(error); //console.log(error);
} }
// Cleanup build caches
try {
await executeDockerCmd({ dockerId, command: `docker builder prune -a -f` })
} catch (error) {
//console.log(error);
}
} }
} }
@@ -1813,4 +2013,42 @@ export function persistentVolumes(id, persistentStorage, config) {
...composeVolumes ...composeVolumes
) || {} ) || {}
return { volumes, volumeMounts } return { volumes, volumeMounts }
}
export function defaultComposeConfiguration(network: string): any {
return {
networks: [network],
restart: 'on-failure',
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 10,
window: '120s'
}
}
}
}
export function decryptApplication(application: any) {
if (application) {
if (application?.gitSource?.githubApp?.clientSecret) {
application.gitSource.githubApp.clientSecret = decrypt(application.gitSource.githubApp.clientSecret) || null;
}
if (application?.gitSource?.githubApp?.webhookSecret) {
application.gitSource.githubApp.webhookSecret = decrypt(application.gitSource.githubApp.webhookSecret) || null;
}
if (application?.gitSource?.githubApp?.privateKey) {
application.gitSource.githubApp.privateKey = decrypt(application.gitSource.githubApp.privateKey) || null;
}
if (application?.gitSource?.gitlabApp?.appSecret) {
application.gitSource.gitlabApp.appSecret = decrypt(application.gitSource.gitlabApp.appSecret) || null;
}
if (application?.secrets.length > 0) {
application.secrets = application.secrets.map((s: any) => {
s.value = decrypt(s.value) || null
return s;
});
}
return application;
}
} }

View File

@@ -71,7 +71,6 @@ export async function removeContainer({
}): Promise<void> { }): Promise<void> {
try { try {
const { stdout } = await executeDockerCmd({ dockerId, command: `docker inspect --format '{{json .State}}' ${id}` }) const { stdout } = await executeDockerCmd({ dockerId, command: `docker inspect --format '{{json .State}}' ${id}` })
console.log(id)
if (JSON.parse(stdout).Running) { if (JSON.parse(stdout).Running) {
await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` }) await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` })
await executeDockerCmd({ dockerId, command: `docker rm ${id}` }) await executeDockerCmd({ dockerId, command: `docker rm ${id}` })

View File

@@ -12,7 +12,8 @@ export default async function ({
htmlUrl, htmlUrl,
branch, branch,
buildId, buildId,
customPort customPort,
forPublic
}: { }: {
applicationId: string; applicationId: string;
workdir: string; workdir: string;
@@ -23,41 +24,55 @@ export default async function ({
branch: string; branch: string;
buildId: string; buildId: string;
customPort: number; customPort: number;
forPublic?: boolean;
}): Promise<string> { }): Promise<string> {
const { default: got } = await import('got') const { default: got } = await import('got')
const url = htmlUrl.replace('https://', '').replace('http://', ''); const url = htmlUrl.replace('https://', '').replace('http://', '');
await saveBuildLog({ line: 'GitHub importer started.', buildId, applicationId }); await saveBuildLog({ line: 'GitHub importer started.', buildId, applicationId });
if (forPublic) {
await saveBuildLog({
line: `Cloning ${repository}:${branch} branch.`,
buildId,
applicationId
});
await asyncExecShell(
`git clone -q -b ${branch} https://${url}/${repository}.git ${workdir}/ && cd ${workdir} && git submodule update --init --recursive && git lfs pull && cd .. `
);
const body = await prisma.githubApp.findUnique({ where: { id: githubAppId } }); } else {
if (body.privateKey) body.privateKey = decrypt(body.privateKey); const body = await prisma.githubApp.findUnique({ where: { id: githubAppId } });
const { privateKey, appId, installationId } = body if (body.privateKey) body.privateKey = decrypt(body.privateKey);
const { privateKey, appId, installationId } = body
const githubPrivateKey = privateKey.replace(/\\n/g, '\n').replace(/"/g, '');
const githubPrivateKey = privateKey.replace(/\\n/g, '\n').replace(/"/g, ''); const payload = {
iat: Math.round(new Date().getTime() / 1000),
const payload = { exp: Math.round(new Date().getTime() / 1000 + 60),
iat: Math.round(new Date().getTime() / 1000), iss: appId
exp: Math.round(new Date().getTime() / 1000 + 60), };
iss: appId const jwtToken = jsonwebtoken.sign(payload, githubPrivateKey, {
}; algorithm: 'RS256'
const jwtToken = jsonwebtoken.sign(payload, githubPrivateKey, { });
algorithm: 'RS256' const { token } = await got
}); .post(`${apiUrl}/app/installations/${installationId}/access_tokens`, {
const { token } = await got headers: {
.post(`${apiUrl}/app/installations/${installationId}/access_tokens`, { Authorization: `Bearer ${jwtToken}`,
headers: { Accept: 'application/vnd.github.machine-man-preview+json'
Authorization: `Bearer ${jwtToken}`, }
Accept: 'application/vnd.github.machine-man-preview+json' })
} .json();
}) await saveBuildLog({
.json(); line: `Cloning ${repository}:${branch} branch.`,
await saveBuildLog({ buildId,
line: `Cloning ${repository}:${branch} branch.`, applicationId
buildId, });
applicationId await asyncExecShell(
}); `git clone -q -b ${branch} https://x-access-token:${token}@${url}/${repository}.git --config core.sshCommand="ssh -p ${customPort}" ${workdir}/ && cd ${workdir} && git submodule update --init --recursive && git lfs pull && cd .. `
await asyncExecShell( );
`git clone -q -b ${branch} https://x-access-token:${token}@${url}/${repository}.git --config core.sshCommand="ssh -p ${customPort}" ${workdir}/ && cd ${workdir} && git submodule update --init --recursive && git lfs pull && cd .. ` }
);
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', '');
} }

View File

@@ -2,54 +2,29 @@ import Bree from 'bree';
import path from 'path'; import path from 'path';
import Cabin from 'cabin'; import Cabin from 'cabin';
import TSBree from '@breejs/ts-worker'; import TSBree from '@breejs/ts-worker';
import { isDev } from './common';
export const isDev = process.env.NODE_ENV === 'development';
Bree.extend(TSBree); Bree.extend(TSBree);
const options: any = { const options: any = {
defaultExtension: 'js', defaultExtension: 'js',
// logger: new Cabin(),
logger: false, logger: false,
workerMessageHandler: async ({ name, message }) => { workerMessageHandler: async ({ name, message }) => {
if (name === 'deployApplication') { if (name === 'deployApplication' && message?.deploying) {
if (message.pending === 0 && message.size === 0) { if (scheduler.workers.has('autoUpdater') || scheduler.workers.has('cleanupStorage')) {
if (message.caller === 'autoUpdater') { scheduler.workers.get('deployApplication').postMessage('cancel')
if (!scheduler.workers.has('autoUpdater')) {
await scheduler.stop('deployApplication');
await scheduler.run('autoUpdater')
}
}
if (message.caller === 'cleanupStorage') {
if (!scheduler.workers.has('cleanupStorage')) {
await scheduler.run('cleanupStorage')
}
}
} }
} }
}, },
jobs: [ jobs: [
{ { name: 'infrastructure' },
name: 'deployApplication' { name: 'deployApplication' },
},
{
name: 'cleanupStorage',
},
{
name: 'cleanupPrismaEngines',
interval: '1m'
},
{
name: 'checkProxies',
interval: '10s'
},
{
name: 'autoUpdater',
}
], ],
}; };
if (isDev) options.root = path.join(__dirname, '../jobs'); if (isDev) options.root = path.join(__dirname, '../jobs');
export const scheduler = new Bree(options); export const scheduler = new Bree(options);

View File

@@ -557,4 +557,134 @@ export const appwrite = [{
isNumber: false, isNumber: false,
isBoolean: false, isBoolean: false,
isEncrypted: false isEncrypted: false
}]
export const glitchTip = [{
name: 'postgresqlUser',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'postgresqlPassword',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'postgresqlDatabase',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'postgresqlPublicPort',
isEditable: false,
isLowerCase: false,
isNumber: true,
isBoolean: false,
isEncrypted: false
},
{
name: 'secretKeyBase',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'defaultEmail',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'defaultUsername',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'defaultPassword',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'defaultFromEmail',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'emailUrl',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'emailBackend',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'mailgunApiKey',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'sendgridApiKey',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'enableOpenUserRegistration',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: true,
isEncrypted: false
}]
export const searxng = [{
name: 'secretKey',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'redisPassword',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
}] }]

View File

@@ -17,19 +17,4 @@ export async function defaultServiceConfigurations({ id, teamId }) {
}); });
} }
return { ...service, network, port, workdir, image, secrets } return { ...service, network, port, workdir, image, secrets }
}
export function defaultServiceComposeConfiguration(network: string): any {
return {
networks: [network],
restart: 'always',
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '10s',
max_attempts: 10,
window: '120s'
}
}
}
} }

View File

@@ -5,7 +5,7 @@ import axios from 'axios';
import { FastifyReply } from 'fastify'; import { FastifyReply } from 'fastify';
import { day } from '../../../../lib/dayjs'; import { day } from '../../../../lib/dayjs';
import { setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common'; import { setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common';
import { checkDomainsIsValidInDNS, checkDoubleBranch, decrypt, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, getFreeExposedPort, isDev, isDomainConfigured, listSettings, prisma, stopBuild, uniqueName } from '../../../../lib/common'; import { checkDomainsIsValidInDNS, checkDoubleBranch, checkExposedPort, decrypt, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, getFreeExposedPort, isDev, isDomainConfigured, listSettings, prisma, stopBuild, uniqueName } from '../../../../lib/common';
import { checkContainer, formatLabelsOnDocker, isContainerExited, removeContainer } from '../../../../lib/docker'; import { checkContainer, formatLabelsOnDocker, isContainerExited, removeContainer } from '../../../../lib/docker';
import { scheduler } from '../../../../lib/scheduler'; import { scheduler } from '../../../../lib/scheduler';
@@ -34,7 +34,7 @@ export async function getImages(request: FastifyRequest<GetImages>) {
const { buildPack, deploymentType } = request.body const { buildPack, deploymentType } = request.body
let publishDirectory = undefined; let publishDirectory = undefined;
let port = undefined let port = undefined
const { baseImage, baseBuildImage, baseBuildImages, baseImages, } = setDefaultBaseImage( const { baseImage, baseBuildImage, baseBuildImages, baseImages } = setDefaultBaseImage(
buildPack, deploymentType buildPack, deploymentType
); );
if (buildPack === 'nextjs') { if (buildPack === 'nextjs') {
@@ -56,8 +56,7 @@ export async function getImages(request: FastifyRequest<GetImages>) {
} }
} }
return { baseImage, baseImages, baseBuildImage, baseBuildImages, publishDirectory, port }
return { baseBuildImage, baseBuildImages, publishDirectory, port }
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
@@ -75,7 +74,6 @@ export async function getApplicationStatus(request: FastifyRequest<OnlyId>) {
isExited = await isContainerExited(application.destinationDocker.id, id); isExited = await isContainerExited(application.destinationDocker.id, id);
} }
return { return {
isQueueActive: scheduler.workers.has('deployApplication'),
isRunning, isRunning,
isExited, isExited,
}; };
@@ -233,11 +231,13 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
baseBuildImage, baseBuildImage,
deploymentType deploymentType
} = request.body } = request.body
if (port) port = Number(port); if (port) port = Number(port);
if (exposePort) { if (exposePort) {
exposePort = Number(exposePort); exposePort = Number(exposePort);
} }
const { destinationDocker: { id: dockerId, remoteIpAddress } } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } })
if (exposePort) await checkExposedPort({ id, exposePort, dockerId, remoteIpAddress })
if (denoOptions) denoOptions = denoOptions.trim(); if (denoOptions) denoOptions = denoOptions.trim();
const defaultConfiguration = await setDefaultConfiguration({ const defaultConfiguration = await setDefaultConfiguration({
buildPack, buildPack,
@@ -392,18 +392,7 @@ export async function checkDNS(request: FastifyRequest<CheckDNS>) {
if (found) { if (found) {
throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` } throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` }
} }
if (exposePort) { if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, dockerId, remoteIpAddress })
if (exposePort < 1024 || exposePort > 65535) {
throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` }
}
if (configuredPort !== exposePort) {
const availablePort = await getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress);
if (availablePort.toString() !== exposePort.toString()) {
throw { status: 500, message: `Port ${exposePort} is already in use.` }
}
}
}
if (isDNSCheckEnabled && !isDev && !forceSave) { if (isDNSCheckEnabled && !isDev && !forceSave) {
let hostname = request.hostname.split(':')[0]; let hostname = request.hostname.split(':')[0];
if (remoteEngine) hostname = remoteIpAddress; if (remoteEngine) hostname = remoteIpAddress;
@@ -436,7 +425,7 @@ export async function deployApplication(request: FastifyRequest<DeployApplicatio
try { try {
const { id } = request.params const { id } = request.params
const teamId = request.user?.teamId; const teamId = request.user?.teamId;
const { pullmergeRequestId = null, branch } = request.body const { pullmergeRequestId = null, branch, forceRebuild } = request.body
const buildId = cuid(); const buildId = cuid();
const application = await getApplicationFromDB(id, teamId); const application = await getApplicationFromDB(id, teamId);
if (application) { if (application) {
@@ -460,7 +449,10 @@ export async function deployApplication(request: FastifyRequest<DeployApplicatio
data: { data: {
id: buildId, id: buildId,
applicationId: id, applicationId: id,
sourceBranch: branch,
branch: application.branch, branch: application.branch,
pullmergeRequestId,
forceRebuild,
destinationDockerId: application.destinationDocker?.id, destinationDockerId: application.destinationDocker?.id,
gitSourceId: application.gitSource?.id, gitSourceId: application.gitSource?.id,
githubAppId: application.gitSource?.githubApp?.id, githubAppId: application.gitSource?.githubApp?.id,
@@ -469,22 +461,6 @@ export async function deployApplication(request: FastifyRequest<DeployApplicatio
type: 'manual' type: 'manual'
} }
}); });
if (pullmergeRequestId) {
scheduler.workers.get('deployApplication').postMessage({
build_id: buildId,
type: 'manual',
...application,
sourceBranch: branch,
pullmergeRequestId
});
} else {
scheduler.workers.get('deployApplication').postMessage({
build_id: buildId,
type: 'manual',
...application
});
}
return { return {
buildId buildId
}; };
@@ -499,11 +475,20 @@ export async function deployApplication(request: FastifyRequest<DeployApplicatio
export async function saveApplicationSource(request: FastifyRequest<SaveApplicationSource>, reply: FastifyReply) { export async function saveApplicationSource(request: FastifyRequest<SaveApplicationSource>, reply: FastifyReply) {
try { try {
const { id } = request.params const { id } = request.params
const { gitSourceId } = request.body const { gitSourceId, forPublic, type } = request.body
await prisma.application.update({ if (forPublic) {
where: { id }, const publicGit = await prisma.gitSource.findFirst({ where: { type, forPublic } });
data: { gitSource: { connect: { id: gitSourceId } } } await prisma.application.update({
}); where: { id },
data: { gitSource: { connect: { id: publicGit.id } } }
});
} else {
await prisma.application.update({
where: { id },
data: { gitSource: { connect: { id: gitSourceId } } }
});
}
return reply.code(201).send() return reply.code(201).send()
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
@@ -557,7 +542,7 @@ export async function checkRepository(request: FastifyRequest<CheckRepository>)
export async function saveRepository(request, reply) { export async function saveRepository(request, reply) {
try { try {
const { id } = request.params const { id } = request.params
let { repository, branch, projectId, autodeploy, webhookToken } = request.body let { repository, branch, projectId, autodeploy, webhookToken, isPublicRepository = false } = request.body
repository = repository.toLowerCase(); repository = repository.toLowerCase();
branch = branch.toLowerCase(); branch = branch.toLowerCase();
@@ -565,17 +550,19 @@ export async function saveRepository(request, reply) {
if (webhookToken) { if (webhookToken) {
await prisma.application.update({ await prisma.application.update({
where: { id }, where: { id },
data: { repository, branch, projectId, gitSource: { update: { gitlabApp: { update: { webhookToken: webhookToken ? webhookToken : undefined } } } }, settings: { update: { autodeploy } } } data: { repository, branch, projectId, gitSource: { update: { gitlabApp: { update: { webhookToken: webhookToken ? webhookToken : undefined } } } }, settings: { update: { autodeploy, isPublicRepository } } }
}); });
} else { } else {
await prisma.application.update({ await prisma.application.update({
where: { id }, where: { id },
data: { repository, branch, projectId, settings: { update: { autodeploy } } } data: { repository, branch, projectId, settings: { update: { autodeploy, isPublicRepository } } }
}); });
} }
const isDouble = await checkDoubleBranch(branch, projectId); if (!isPublicRepository) {
if (isDouble) { const isDouble = await checkDoubleBranch(branch, projectId);
await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false } }) if (isDouble) {
await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false, isPublicRepository } })
}
} }
return reply.code(201).send() return reply.code(201).send()
} catch ({ status, message }) { } catch ({ status, message }) {
@@ -607,7 +594,8 @@ export async function getBuildPack(request) {
projectId: application.projectId, projectId: application.projectId,
repository: application.repository, repository: application.repository,
branch: application.branch, branch: application.branch,
apiUrl: application.gitSource.apiUrl apiUrl: application.gitSource.apiUrl,
isPublicRepository: application.settings.isPublicRepository
} }
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
@@ -657,13 +645,13 @@ export async function saveSecret(request: FastifyRequest<SaveSecret>, reply: Fas
if (found) { if (found) {
throw { status: 500, message: `Secret ${name} already exists.` } throw { status: 500, message: `Secret ${name} already exists.` }
} else { } else {
value = encrypt(value); value = encrypt(value.trim());
await prisma.secret.create({ await prisma.secret.create({
data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } } data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } }
}); });
} }
} else { } else {
value = encrypt(value); value = encrypt(value.trim());
const found = await prisma.secret.findFirst({ where: { applicationId: id, name, isPRMRSecret } }); const found = await prisma.secret.findFirst({ where: { applicationId: id, name, isPRMRSecret } });
if (found) { if (found) {

View File

@@ -44,13 +44,13 @@ export interface CheckDNS extends OnlyId {
} }
export interface DeployApplication { export interface DeployApplication {
Querystring: { domain: string } Querystring: { domain: string }
Body: { pullmergeRequestId: string | null, branch: string } Body: { pullmergeRequestId: string | null, branch: string, forceRebuild?: boolean }
} }
export interface GetImages { export interface GetImages {
Body: { buildPack: string, deploymentType: string } Body: { buildPack: string, deploymentType: string }
} }
export interface SaveApplicationSource extends OnlyId { export interface SaveApplicationSource extends OnlyId {
Body: { gitSourceId: string } Body: { gitSourceId?: string | null, forPublic?: boolean, type?: string }
} }
export interface CheckRepository extends OnlyId { export interface CheckRepository extends OnlyId {
Querystring: { repository: string, branch: string } Querystring: { repository: string, branch: string }
@@ -115,7 +115,8 @@ export interface CancelDeployment {
export interface DeployApplication extends OnlyId { export interface DeployApplication extends OnlyId {
Body: { Body: {
pullmergeRequestId: string | null, pullmergeRequestId: string | null,
branch: string branch: string,
forceRebuild?: boolean
} }
} }

View File

@@ -29,9 +29,9 @@ export async function newDatabase(request: FastifyRequest, reply: FastifyReply)
const name = uniqueName(); const name = uniqueName();
const dbUser = cuid(); const dbUser = cuid();
const dbUserPassword = encrypt(generatePassword()); const dbUserPassword = encrypt(generatePassword({}));
const rootUser = cuid(); const rootUser = cuid();
const rootUserPassword = encrypt(generatePassword()); const rootUserPassword = encrypt(generatePassword({}));
const defaultDatabase = cuid(); const defaultDatabase = cuid();
const { id } = await prisma.database.create({ const { id } = await prisma.database.create({
@@ -433,9 +433,13 @@ export async function saveDatabaseSettings(request: FastifyRequest<SaveDatabaseS
const { id } = request.params; const { id } = request.params;
const { isPublic, appendOnly = true } = request.body; const { isPublic, appendOnly = true } = request.body;
const { destinationDocker: { id: dockerId } } = await prisma.database.findUnique({ where: { id }, include: { destinationDocker: true } }) let publicPort = null
const publicPort = await getFreePublicPort(id, dockerId);
const { destinationDocker: { id: dockerId } } = await prisma.database.findUnique({ where: { id }, include: { destinationDocker: true } })
if (isPublic) {
publicPort = await getFreePublicPort(id, dockerId);
}
await prisma.database.update({ await prisma.database.update({
where: { id }, where: { id },
data: { data: {

View File

@@ -79,7 +79,6 @@ export async function newDestination(request: FastifyRequest<NewDestination>, re
let { name, network, engine, isCoolifyProxyUsed, remoteIpAddress, remoteUser, remotePort } = request.body let { name, network, engine, isCoolifyProxyUsed, remoteIpAddress, remoteUser, remotePort } = request.body
if (id === 'new') { if (id === 'new') {
console.log(engine)
if (engine) { if (engine) {
const { stdout } = await asyncExecShell(`DOCKER_HOST=unix:///var/run/docker.sock docker network ls --filter 'name=^${network}$' --format '{{json .}}'`); const { stdout } = await asyncExecShell(`DOCKER_HOST=unix:///var/run/docker.sock docker network ls --filter 'name=^${network}$' --format '{{json .}}'`);
if (stdout === '') { if (stdout === '') {

View File

@@ -73,6 +73,23 @@ export async function update(request: FastifyRequest<Update>) {
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function restartCoolify(request: FastifyRequest<any>) {
try {
const teamId = request.user.teamId;
if (teamId === '0') {
if (!isDev) {
await asyncExecShell(`docker restart coolify`);
return {};
} else {
console.log('Restarting Coolify')
return {};
}
}
throw { status: 500, message: 'You are not authorized to restart Coolify.' };
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function showUsage() { export async function showUsage() {
try { try {
return { return {
@@ -101,7 +118,8 @@ export async function showDashboard(request: FastifyRequest) {
include: { settings: true } include: { settings: true }
}); });
const databases = await prisma.database.findMany({ const databases = await prisma.database.findMany({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } } where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { settings: true }
}); });
const services = await prisma.service.findMany({ const services = await prisma.service.findMany({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } } where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }

View File

@@ -158,8 +158,11 @@ export async function getTeam(request: FastifyRequest<OnlyId>, reply: FastifyRep
}); });
const team = await prisma.team.findUnique({ where: { id }, include: { permissions: true } }); const team = await prisma.team.findUnique({ where: { id }, include: { permissions: true } });
const invitations = await prisma.teamInvitation.findMany({ where: { teamId: team.id } }); const invitations = await prisma.teamInvitation.findMany({ where: { teamId: team.id } });
const { teams } = await prisma.user.findUnique({ where: { id: userId }, include: { teams: true } })
return { return {
currentTeam: teamId,
team, team,
teams,
permissions, permissions,
invitations invitations
}; };
@@ -275,10 +278,10 @@ export async function inviteToTeam(request: FastifyRequest<InviteToTeam>, reply:
if (!userFound) { if (!userFound) {
throw { throw {
message: `No user found with '${email}' email address.` message: `No user found with '${email}' email address.`
}; };
} }
const uid = userFound.id; const uid = userFound.id;
if (uid === userId) { if (uid === userId) {
throw { throw {
message: `Invitation to yourself? Whaaaaat?` message: `Invitation to yourself? Whaaaaat?`
}; };

View File

@@ -1,5 +1,5 @@
import { FastifyPluginAsync } from 'fastify'; import { FastifyPluginAsync } from 'fastify';
import { checkUpdate, login, showDashboard, update, showUsage, getCurrentUser, cleanupManually } from './handlers'; import { checkUpdate, login, showDashboard, update, showUsage, getCurrentUser, cleanupManually, restartCoolify } from './handlers';
import { GetCurrentUser } from './types'; import { GetCurrentUser } from './types';
export interface Update { export interface Update {
@@ -47,6 +47,10 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
onRequest: [fastify.authenticate] onRequest: [fastify.authenticate]
}, async () => await showUsage()); }, async () => await showUsage());
fastify.post('/internal/restart', {
onRequest: [fastify.authenticate]
}, async (request) => await restartCoolify(request));
fastify.post('/internal/cleanup', { fastify.post('/internal/cleanup', {
onRequest: [fastify.authenticate] onRequest: [fastify.authenticate]
}, async () => await cleanupManually()); }, async () => await cleanupManually());

View File

@@ -2,14 +2,14 @@ import type { FastifyReply, FastifyRequest } from 'fastify';
import fs from 'fs/promises'; import fs from 'fs/promises';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { prisma, uniqueName, asyncExecShell, getServiceImage, configureServiceType, getServiceFromDB, getContainerUsage, removeService, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, getServiceMainPort, createDirectories, ComposeFile, makeLabelForServices, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, supportedServiceTypesAndVersions, executeDockerCmd, listSettings, getFreeExposedPort, checkDomainsIsValidInDNS, persistentVolumes, asyncSleep, isARM } from '../../../../lib/common'; import { prisma, uniqueName, asyncExecShell, getServiceImage, configureServiceType, getServiceFromDB, getContainerUsage, removeService, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, getServiceMainPort, createDirectories, ComposeFile, makeLabelForServices, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, supportedServiceTypesAndVersions, executeDockerCmd, listSettings, getFreeExposedPort, checkDomainsIsValidInDNS, persistentVolumes, asyncSleep, isARM, defaultComposeConfiguration, checkExposedPort } from '../../../../lib/common';
import { day } from '../../../../lib/dayjs'; import { day } from '../../../../lib/dayjs';
import { checkContainer, isContainerExited, removeContainer } from '../../../../lib/docker'; import { checkContainer, isContainerExited, removeContainer } from '../../../../lib/docker';
import cuid from 'cuid'; import cuid from 'cuid';
import type { OnlyId } from '../../../../types'; import type { OnlyId } from '../../../../types';
import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetWordpressSettings } from './types'; import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetWordpressSettings } from './types';
import { defaultServiceComposeConfiguration, defaultServiceConfigurations } from '../../../../lib/services'; import { defaultServiceConfigurations } from '../../../../lib/services';
// async function startServiceNew(request: FastifyRequest<OnlyId>) { // async function startServiceNew(request: FastifyRequest<OnlyId>) {
// try { // try {
@@ -30,7 +30,7 @@ import { defaultServiceComposeConfiguration, defaultServiceConfigurations } from
// serviceSecret.forEach((secret) => { // serviceSecret.forEach((secret) => {
// environmentVariables[secret.name] = secret.value; // environmentVariables[secret.name] = secret.value;
// }); // });
// } // }
// config.newVolumes = {} // config.newVolumes = {}
// for (const service of Object.entries(config.services)) { // for (const service of Object.entries(config.services)) {
// const name = service[0] // const name = service[0]
@@ -98,7 +98,7 @@ import { defaultServiceComposeConfiguration, defaultServiceConfigurations } from
// } // }
// console.log(config.services) // console.log(config.services)
// console.log(config.volumes) // console.log(config.volumes)
// // config.services[id] = JSON.parse(JSON.stringify(config.services[type])) // // config.services[id] = JSON.parse(JSON.stringify(config.services[type]))
// // config.services[id].container_name = id // // config.services[id].container_name = id
@@ -378,18 +378,7 @@ export async function checkService(request: FastifyRequest<CheckService>) {
} }
} }
} }
if (exposePort) { if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, dockerId, remoteIpAddress })
if (exposePort < 1024 || exposePort > 65535) {
throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` }
}
if (configuredPort !== exposePort) {
const availablePort = await getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress);
if (availablePort.toString() !== exposePort.toString()) {
throw { status: 500, message: `Port ${exposePort} is already in use.` }
}
}
}
if (isDNSCheckEnabled && !isDev && !forceSave) { if (isDNSCheckEnabled && !isDev && !forceSave) {
let hostname = request.hostname.split(':')[0]; let hostname = request.hostname.split(':')[0];
if (remoteEngine) hostname = remoteIpAddress; if (remoteEngine) hostname = remoteIpAddress;
@@ -458,13 +447,13 @@ export async function saveServiceSecret(request: FastifyRequest<SaveServiceSecre
if (found) { if (found) {
throw `Secret ${name} already exists.` throw `Secret ${name} already exists.`
} else { } else {
value = encrypt(value); value = encrypt(value.trim());
await prisma.serviceSecret.create({ await prisma.serviceSecret.create({
data: { name, value, service: { connect: { id } } } data: { name, value, service: { connect: { id } } }
}); });
} }
} else { } else {
value = encrypt(value); value = encrypt(value.trim());
const found = await prisma.serviceSecret.findFirst({ where: { serviceId: id, name } }); const found = await prisma.serviceSecret.findFirst({ where: { serviceId: id, name } });
if (found) { if (found) {
@@ -591,6 +580,12 @@ export async function startService(request: FastifyRequest<ServiceStartStop>) {
if (type === 'appwrite') { if (type === 'appwrite') {
return await startAppWriteService(request) return await startAppWriteService(request)
} }
if (type === 'glitchTip') {
return await startGlitchTipService(request)
}
if (type === 'searxng') {
return await startSearXNGService(request)
}
throw `Service type ${type} not supported.` throw `Service type ${type} not supported.`
} catch (error) { } catch (error) {
throw { status: 500, message: error?.message || error } throw { status: 500, message: error?.message || error }
@@ -599,53 +594,6 @@ export async function startService(request: FastifyRequest<ServiceStartStop>) {
export async function stopService(request: FastifyRequest<ServiceStartStop>) { export async function stopService(request: FastifyRequest<ServiceStartStop>) {
try { try {
return await stopServiceContainers(request) return await stopServiceContainers(request)
// const { type } = request.params
// if (type === 'plausibleanalytics') {
// return await stopPlausibleAnalyticsService(request)
// }
// if (type === 'nocodb') {
// return await stopNocodbService(request)
// }
// if (type === 'minio') {
// return await stopMinioService(request)
// }
// if (type === 'vscodeserver') {
// return await stopVscodeService(request)
// }
// if (type === 'wordpress') {
// return await stopWordpressService(request)
// }
// if (type === 'vaultwarden') {
// return await stopVaultwardenService(request)
// }
// if (type === 'languagetool') {
// return await stopLanguageToolService(request)
// }
// if (type === 'n8n') {
// return await stopN8nService(request)
// }
// if (type === 'uptimekuma') {
// return await stopUptimekumaService(request)
// }
// if (type === 'ghost') {
// return await stopGhostService(request)
// }
// if (type === 'meilisearch') {
// return await stopMeilisearchService(request)
// }
// if (type === 'umami') {
// return await stopUmamiService(request)
// }
// if (type === 'hasura') {
// return await stopHasuraService(request)
// }
// if (type === 'fider') {
// return await stopFiderService(request)
// }
// if (type === 'moodle') {
// return await stopMoodleService(request)
// }
// throw `Service type ${type} not supported.`
} catch (error) { } catch (error) {
throw { status: 500, message: error?.message || error } throw { status: 500, message: error?.message || error }
} }
@@ -806,21 +754,21 @@ COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`;
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
depends_on: [`${id}-postgresql`, `${id}-clickhouse`], depends_on: [`${id}-postgresql`, `${id}-clickhouse`],
labels: makeLabelForServices('plausibleAnalytics'), labels: makeLabelForServices('plausibleAnalytics'),
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
}, },
[`${id}-postgresql`]: { [`${id}-postgresql`]: {
container_name: `${id}-postgresql`, container_name: `${id}-postgresql`,
image: config.postgresql.image, image: config.postgresql.image,
environment: config.postgresql.environmentVariables, environment: config.postgresql.environmentVariables,
volumes: [config.postgresql.volume], volumes: [config.postgresql.volume],
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
}, },
[`${id}-clickhouse`]: { [`${id}-clickhouse`]: {
build: workdir, build: workdir,
container_name: `${id}-clickhouse`, container_name: `${id}-clickhouse`,
environment: config.clickhouse.environmentVariables, environment: config.clickhouse.environmentVariables,
volumes: [config.clickhouse.volume], volumes: [config.clickhouse.volume],
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
} }
}, },
networks: { networks: {
@@ -881,7 +829,7 @@ async function startNocodbService(request: FastifyRequest<ServiceStartStop>) {
environment: config.environmentVariables, environment: config.environmentVariables,
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
labels: makeLabelForServices('nocodb'), labels: makeLabelForServices('nocodb'),
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
} }
}, },
networks: { networks: {
@@ -953,7 +901,7 @@ async function startMinioService(request: FastifyRequest<ServiceStartStop>) {
volumes, volumes,
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
labels: makeLabelForServices('minio'), labels: makeLabelForServices('minio'),
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
} }
}, },
networks: { networks: {
@@ -1019,7 +967,7 @@ async function startVscodeService(request: FastifyRequest<ServiceStartStop>) {
volumes, volumes,
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
labels: makeLabelForServices('vscodeServer'), labels: makeLabelForServices('vscodeServer'),
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
} }
}, },
networks: { networks: {
@@ -1116,7 +1064,7 @@ async function startWordpressService(request: FastifyRequest<ServiceStartStop>)
const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.wordpress) const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.wordpress)
let composeFile: ComposeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
[id]: { [id]: {
@@ -1126,7 +1074,7 @@ async function startWordpressService(request: FastifyRequest<ServiceStartStop>)
volumes, volumes,
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
labels: makeLabelForServices('wordpress'), labels: makeLabelForServices('wordpress'),
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
} }
}, },
networks: { networks: {
@@ -1143,7 +1091,7 @@ async function startWordpressService(request: FastifyRequest<ServiceStartStop>)
image: config.mysql.image, image: config.mysql.image,
volumes: [config.mysql.volume], volumes: [config.mysql.volume],
environment: config.mysql.environmentVariables, environment: config.mysql.environmentVariables,
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
}; };
composeFile.volumes[config.mysql.volume.split(':')[0]] = { composeFile.volumes[config.mysql.volume.split(':')[0]] = {
@@ -1196,7 +1144,7 @@ async function startVaultwardenService(request: FastifyRequest<ServiceStartStop>
volumes, volumes,
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
labels: makeLabelForServices('vaultWarden'), labels: makeLabelForServices('vaultWarden'),
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
} }
}, },
networks: { networks: {
@@ -1252,7 +1200,7 @@ async function startLanguageToolService(request: FastifyRequest<ServiceStartStop
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
volumes, volumes,
labels: makeLabelForServices('languagetool'), labels: makeLabelForServices('languagetool'),
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
} }
}, },
networks: { networks: {
@@ -1309,7 +1257,7 @@ async function startN8nService(request: FastifyRequest<ServiceStartStop>) {
environment: config.environmentVariables, environment: config.environmentVariables,
labels: makeLabelForServices('n8n'), labels: makeLabelForServices('n8n'),
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
} }
}, },
networks: { networks: {
@@ -1364,7 +1312,7 @@ async function startUptimekumaService(request: FastifyRequest<ServiceStartStop>)
environment: config.environmentVariables, environment: config.environmentVariables,
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
labels: makeLabelForServices('uptimekuma'), labels: makeLabelForServices('uptimekuma'),
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
} }
}, },
networks: { networks: {
@@ -1463,14 +1411,14 @@ async function startGhostService(request: FastifyRequest<ServiceStartStop>) {
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
labels: makeLabelForServices('ghost'), labels: makeLabelForServices('ghost'),
depends_on: [`${id}-mariadb`], depends_on: [`${id}-mariadb`],
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
}, },
[`${id}-mariadb`]: { [`${id}-mariadb`]: {
container_name: `${id}-mariadb`, container_name: `${id}-mariadb`,
image: config.mariadb.image, image: config.mariadb.image,
volumes: [config.mariadb.volume], volumes: [config.mariadb.volume],
environment: config.mariadb.environmentVariables, environment: config.mariadb.environmentVariables,
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
} }
}, },
networks: { networks: {
@@ -1536,7 +1484,7 @@ async function startMeilisearchService(request: FastifyRequest<ServiceStartStop>
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
volumes, volumes,
labels: makeLabelForServices('meilisearch'), labels: makeLabelForServices('meilisearch'),
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
} }
}, },
networks: { networks: {
@@ -1702,14 +1650,14 @@ async function startUmamiService(request: FastifyRequest<ServiceStartStop>) {
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
labels: makeLabelForServices('umami'), labels: makeLabelForServices('umami'),
depends_on: [`${id}-postgresql`], depends_on: [`${id}-postgresql`],
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
}, },
[`${id}-postgresql`]: { [`${id}-postgresql`]: {
build: workdir, build: workdir,
container_name: `${id}-postgresql`, container_name: `${id}-postgresql`,
environment: config.postgresql.environmentVariables, environment: config.postgresql.environmentVariables,
volumes: [config.postgresql.volume], volumes: [config.postgresql.volume],
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
} }
}, },
networks: { networks: {
@@ -1789,14 +1737,14 @@ async function startHasuraService(request: FastifyRequest<ServiceStartStop>) {
labels: makeLabelForServices('hasura'), labels: makeLabelForServices('hasura'),
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
depends_on: [`${id}-postgresql`], depends_on: [`${id}-postgresql`],
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
}, },
[`${id}-postgresql`]: { [`${id}-postgresql`]: {
image: config.postgresql.image, image: config.postgresql.image,
container_name: `${id}-postgresql`, container_name: `${id}-postgresql`,
environment: config.postgresql.environmentVariables, environment: config.postgresql.environmentVariables,
volumes: [config.postgresql.volume], volumes: [config.postgresql.volume],
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
} }
}, },
networks: { networks: {
@@ -1902,14 +1850,14 @@ async function startFiderService(request: FastifyRequest<ServiceStartStop>) {
labels: makeLabelForServices('fider'), labels: makeLabelForServices('fider'),
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
depends_on: [`${id}-postgresql`], depends_on: [`${id}-postgresql`],
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
}, },
[`${id}-postgresql`]: { [`${id}-postgresql`]: {
image: config.postgresql.image, image: config.postgresql.image,
container_name: `${id}-postgresql`, container_name: `${id}-postgresql`,
environment: config.postgresql.environmentVariables, environment: config.postgresql.environmentVariables,
volumes: [config.postgresql.volume], volumes: [config.postgresql.volume],
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
} }
}, },
networks: { networks: {
@@ -1995,7 +1943,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
"_APP_STATSD_PORT=8125", "_APP_STATSD_PORT=8125",
...secrets ...secrets
], ],
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
}, },
[`${id}-realtime`]: { [`${id}-realtime`]: {
image: `${image}:${version}`, image: `${image}:${version}`,
@@ -2018,7 +1966,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
`_APP_DB_PASS=${mariadbPassword}`, `_APP_DB_PASS=${mariadbPassword}`,
...secrets ...secrets
], ],
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
}, },
[`${id}-worker-audits`]: { [`${id}-worker-audits`]: {
@@ -2042,7 +1990,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
`_APP_DB_PASS=${mariadbPassword}`, `_APP_DB_PASS=${mariadbPassword}`,
...secrets ...secrets
], ],
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
}, },
[`${id}-worker-webhooks`]: { [`${id}-worker-webhooks`]: {
image: `${image}:${version}`, image: `${image}:${version}`,
@@ -2060,7 +2008,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
"_APP_REDIS_PORT=6379", "_APP_REDIS_PORT=6379",
...secrets ...secrets
], ],
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
}, },
[`${id}-worker-deletes`]: { [`${id}-worker-deletes`]: {
image: `${image}:${version}`, image: `${image}:${version}`,
@@ -2093,7 +2041,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
`_APP_EXECUTOR_HOST=http://${id}-executor/v1`, `_APP_EXECUTOR_HOST=http://${id}-executor/v1`,
...secrets ...secrets
], ],
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
}, },
[`${id}-worker-databases`]: { [`${id}-worker-databases`]: {
image: `${image}:${version}`, image: `${image}:${version}`,
@@ -2116,7 +2064,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
`_APP_DB_PASS=${mariadbPassword}`, `_APP_DB_PASS=${mariadbPassword}`,
...secrets ...secrets
], ],
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
}, },
[`${id}-worker-builds`]: { [`${id}-worker-builds`]: {
image: `${image}:${version}`, image: `${image}:${version}`,
@@ -2141,7 +2089,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
`_APP_DB_PASS=${mariadbPassword}`, `_APP_DB_PASS=${mariadbPassword}`,
...secrets ...secrets
], ],
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
}, },
[`${id}-worker-certificates`]: { [`${id}-worker-certificates`]: {
image: `${image}:${version}`, image: `${image}:${version}`,
@@ -2170,7 +2118,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
`_APP_DB_PASS=${mariadbPassword}`, `_APP_DB_PASS=${mariadbPassword}`,
...secrets ...secrets
], ],
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
}, },
[`${id}-worker-functions`]: { [`${id}-worker-functions`]: {
image: `${image}:${version}`, image: `${image}:${version}`,
@@ -2196,7 +2144,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
`_APP_EXECUTOR_HOST=http://${id}-executor/v1`, `_APP_EXECUTOR_HOST=http://${id}-executor/v1`,
...secrets ...secrets
], ],
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
}, },
[`${id}-executor`]: { [`${id}-executor`]: {
image: `${image}:${version}`, image: `${image}:${version}`,
@@ -2220,7 +2168,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
`_APP_EXECUTOR_SECRET=${executorSecret}`, `_APP_EXECUTOR_SECRET=${executorSecret}`,
...secrets ...secrets
], ],
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
}, },
[`${id}-worker-mails`]: { [`${id}-worker-mails`]: {
image: `${image}:${version}`, image: `${image}:${version}`,
@@ -2237,7 +2185,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
"_APP_REDIS_PORT=6379", "_APP_REDIS_PORT=6379",
...secrets ...secrets
], ],
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
}, },
[`${id}-worker-messaging`]: { [`${id}-worker-messaging`]: {
image: `${image}:${version}`, image: `${image}:${version}`,
@@ -2253,7 +2201,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
"_APP_REDIS_PORT=6379", "_APP_REDIS_PORT=6379",
...secrets ...secrets
], ],
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
}, },
[`${id}-maintenance`]: { [`${id}-maintenance`]: {
image: `${image}:${version}`, image: `${image}:${version}`,
@@ -2277,7 +2225,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
`_APP_DB_PASS=${mariadbPassword}`, `_APP_DB_PASS=${mariadbPassword}`,
...secrets ...secrets
], ],
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
}, },
[`${id}-schedule`]: { [`${id}-schedule`]: {
image: `${image}:${version}`, image: `${image}:${version}`,
@@ -2293,7 +2241,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
"_APP_REDIS_PORT=6379", "_APP_REDIS_PORT=6379",
...secrets ...secrets
], ],
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
}, },
[`${id}-mariadb`]: { [`${id}-mariadb`]: {
"image": "mariadb:10.7", "image": "mariadb:10.7",
@@ -2310,7 +2258,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
`MYSQL_DATABASE=${mariadbDatabase}` `MYSQL_DATABASE=${mariadbDatabase}`
], ],
"command": "mysqld --innodb-flush-method=fsync", "command": "mysqld --innodb-flush-method=fsync",
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
}, },
[`${id}-redis`]: { [`${id}-redis`]: {
"image": "redis:6.2-alpine", "image": "redis:6.2-alpine",
@@ -2319,7 +2267,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
"volumes": [ "volumes": [
`${id}-redis:/data:rw` `${id}-redis:/data:rw`
], ],
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
}, },
}; };
@@ -2348,7 +2296,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
"_APP_REDIS_PORT=6379", "_APP_REDIS_PORT=6379",
...secrets ...secrets
], ],
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
} }
dockerCompose[`${id}-influxdb`] = { dockerCompose[`${id}-influxdb`] = {
"image": "appwrite/influxdb:1.5.0", "image": "appwrite/influxdb:1.5.0",
@@ -2356,7 +2304,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
"volumes": [ "volumes": [
`${id}-influxdb:/var/lib/influxdb:rw` `${id}-influxdb:/var/lib/influxdb:rw`
], ],
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
} }
dockerCompose[`${id}-telegraf`] = { dockerCompose[`${id}-telegraf`] = {
"image": "appwrite/telegraf:1.4.0", "image": "appwrite/telegraf:1.4.0",
@@ -2365,7 +2313,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
`_APP_INFLUXDB_HOST=${id}-influxdb`, `_APP_INFLUXDB_HOST=${id}-influxdb`,
"_APP_INFLUXDB_PORT=8086", "_APP_INFLUXDB_PORT=8086",
], ],
...defaultServiceComposeConfiguration(network), ...defaultComposeConfiguration(network),
} }
} }
@@ -2420,6 +2368,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
} }
async function startServiceContainers(dockerId, composeFileDestination) { async function startServiceContainers(dockerId, composeFileDestination) {
await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} pull` }) await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} pull` })
await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} build --no-cache` })
await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} create` }) await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} create` })
await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} start` }) await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} start` })
await asyncSleep(1000); await asyncSleep(1000);
@@ -2574,6 +2523,255 @@ async function startMoodleService(request: FastifyRequest<ServiceStartStop>) {
} }
} }
async function startGlitchTipService(request: FastifyRequest<ServiceStartStop>) {
try {
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const {
type,
version,
fqdn,
destinationDockerId,
destinationDocker,
serviceSecret,
persistentStorage,
exposePort,
glitchTip: {
postgresqlDatabase,
postgresqlPassword,
postgresqlUser,
secretKeyBase,
defaultEmail,
defaultUsername,
defaultPassword,
defaultFromEmail,
emailSmtpHost,
emailSmtpPort,
emailSmtpUser,
emailSmtpPassword,
emailSmtpUseTls,
emailSmtpUseSsl,
emailBackend,
mailgunApiKey,
sendgridApiKey,
enableOpenUserRegistration,
}
} = service;
const network = destinationDockerId && destinationDocker.network;
const port = getServiceMainPort('glitchTip');
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
const config = {
glitchTip: {
image: `${image}:${version}`,
environmentVariables: {
PORT: port,
GLITCHTIP_DOMAIN: fqdn,
SECRET_KEY: secretKeyBase,
DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}`,
REDIS_URL: `redis://${id}-redis:6379/0`,
DEFAULT_FROM_EMAIL: defaultFromEmail,
EMAIL_HOST: emailSmtpHost,
EMAIL_PORT: emailSmtpPort,
EMAIL_HOST_USER: emailSmtpUser,
EMAIL_HOST_PASSWORD: emailSmtpPassword,
EMAIL_USE_TLS: emailSmtpUseTls,
EMAIL_USE_SSL: emailSmtpUseSsl,
EMAIL_BACKEND: emailBackend,
MAILGUN_API_KEY: mailgunApiKey,
SENDGRID_API_KEY: sendgridApiKey,
ENABLE_OPEN_USER_REGISTRATION: enableOpenUserRegistration,
DJANGO_SUPERUSER_EMAIL: defaultEmail,
DJANGO_SUPERUSER_USERNAME: defaultUsername,
DJANGO_SUPERUSER_PASSWORD: defaultPassword,
}
},
postgresql: {
image: 'postgres:14-alpine',
volume: `${id}-postgresql-data:/var/lib/postgresql/data`,
environmentVariables: {
POSTGRES_USER: postgresqlUser,
POSTGRES_PASSWORD: postgresqlPassword,
POSTGRES_DB: postgresqlDatabase
}
},
redis: {
image: 'redis:7-alpine',
volume: `${id}-redis-data:/data`,
}
};
if (serviceSecret.length > 0) {
serviceSecret.forEach((secret) => {
config.glitchTip.environmentVariables[secret.name] = secret.value;
});
}
const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.glitchTip)
const composeFile: ComposeFile = {
version: '3.8',
services: {
[id]: {
container_name: id,
image: config.glitchTip.image,
environment: config.glitchTip.environmentVariables,
volumes,
labels: makeLabelForServices('glitchTip'),
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
depends_on: [`${id}-postgresql`, `${id}-redis`],
...defaultComposeConfiguration(network),
},
[`${id}-worker`]: {
container_name: `${id}-worker`,
image: config.glitchTip.image,
command: './bin/run-celery-with-beat.sh',
environment: config.glitchTip.environmentVariables,
depends_on: [`${id}-postgresql`, `${id}-redis`],
...defaultComposeConfiguration(network),
},
[`${id}-setup`]: {
container_name: `${id}-setup`,
image: config.glitchTip.image,
command: 'sh -c "(./manage.py migrate || true) && (./manage.py createsuperuser --noinput || true)"',
environment: config.glitchTip.environmentVariables,
networks: [network],
restart: "no",
depends_on: [`${id}-postgresql`, `${id}-redis`]
},
[`${id}-postgresql`]: {
image: config.postgresql.image,
container_name: `${id}-postgresql`,
environment: config.postgresql.environmentVariables,
volumes: [config.postgresql.volume],
...defaultComposeConfiguration(network),
},
[`${id}-redis`]: {
image: config.redis.image,
container_name: `${id}-redis`,
volumes: [config.redis.volume],
...defaultComposeConfiguration(network),
}
},
networks: {
[network]: {
external: true
}
},
volumes: {
...volumeMounts,
[config.postgresql.volume.split(':')[0]]: {
name: config.postgresql.volume.split(':')[0]
},
[config.redis.volume.split(':')[0]]: {
name: config.redis.volume.split(':')[0]
}
}
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
async function startSearXNGService(request: FastifyRequest<ServiceStartStop>) {
try {
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage, fqdn, searxng: { secretKey, redisPassword } } =
service;
const network = destinationDockerId && destinationDocker.network;
const port = getServiceMainPort('searxng');
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
const config = {
searxng: {
image: `${image}:${version}`,
volume: `${id}-searxng:/etc/searxng`,
environmentVariables: {
SEARXNG_BASE_URL: `${fqdn}`
},
},
redis: {
image: 'redis:7-alpine',
}
};
const settingsYml = `
# see https://docs.searxng.org/admin/engines/settings.html#use-default-settings
use_default_settings: true
server:
secret_key: ${secretKey}
limiter: true
image_proxy: true
ui:
static_use_hash: true
redis:
url: redis://:${redisPassword}@${id}-redis:6379/0`
const Dockerfile = `
FROM ${config.searxng.image}
COPY ./settings.yml /etc/searxng/settings.yml`;
if (serviceSecret.length > 0) {
serviceSecret.forEach((secret) => {
config.searxng.environmentVariables[secret.name] = secret.value;
});
}
const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config)
const composeFile: ComposeFile = {
version: '3.8',
services: {
[id]: {
build: workdir,
container_name: id,
volumes,
environment: config.searxng.environmentVariables,
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
labels: makeLabelForServices('searxng'),
cap_drop: ['ALL'],
cap_add: ['CHOWN', 'SETGID', 'SETUID', 'DAC_OVERRIDE'],
depends_on: [`${id}-redis`],
...defaultComposeConfiguration(network),
},
[`${id}-redis`]: {
container_name: `${id}-redis`,
image: config.redis.image,
command: `redis-server --requirepass ${redisPassword} --save "" --appendonly "no"`,
labels: makeLabelForServices('searxng'),
cap_drop: ['ALL'],
cap_add: ['SETGID', 'SETUID', 'DAC_OVERRIDE'],
...defaultComposeConfiguration(network),
},
},
networks: {
[network]: {
external: true
}
},
volumes: volumeMounts
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile);
await fs.writeFile(`${workdir}/settings.yml`, settingsYml);
await startServiceContainers(destinationDocker.id, composeFileDestination)
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function activatePlausibleUsers(request: FastifyRequest<OnlyId>, reply: FastifyReply) { export async function activatePlausibleUsers(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
try { try {
const { id } = request.params const { id } = request.params
@@ -2624,7 +2822,7 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
const publicPort = await getFreePublicPort(id, dockerId); const publicPort = await getFreePublicPort(id, dockerId);
let ftpUser = cuid(); let ftpUser = cuid();
let ftpPassword = generatePassword(); let ftpPassword = generatePassword({});
const hostkeyDir = isDev ? '/tmp/hostkeys' : '/app/ssl/hostkeys'; const hostkeyDir = isDev ? '/tmp/hostkeys' : '/app/ssl/hostkeys';
try { try {

View File

@@ -9,7 +9,7 @@ export async function listSources(request: FastifyRequest) {
try { try {
const teamId = request.user?.teamId; const teamId = request.user?.teamId;
const sources = await prisma.gitSource.findMany({ const sources = await prisma.gitSource.findMany({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, where: { teams: { some: { id: teamId === '0' ? undefined : teamId } }, githubApp: { gitSource: { forPublic: false } } },
include: { teams: true, githubApp: true, gitlabApp: true } include: { teams: true, githubApp: true, gitlabApp: true }
}); });
return { return {

View File

@@ -142,17 +142,11 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
type: 'webhook_commit' type: 'webhook_commit'
} }
}); });
scheduler.workers.get('deployApplication').postMessage({
build_id: buildId,
type: 'webhook_commit',
...applicationFound
});
return { return {
message: 'Queued. Thank you!' message: 'Queued. Thank you!'
}; };
} else if (githubEvent === 'pull_request') { } else if (githubEvent === 'pull_request') {
const pullmergeRequestId = body.number; const pullmergeRequestId = body.number.toString();
const pullmergeRequestAction = body.action; const pullmergeRequestAction = body.action;
const sourceBranch = body.pull_request.head.ref.includes('/') ? body.pull_request.head.ref.split('/')[2] : body.pull_request.head.ref; const sourceBranch = body.pull_request.head.ref.includes('/') ? body.pull_request.head.ref.split('/')[2] : body.pull_request.head.ref;
if (!allowedActions.includes(pullmergeRequestAction)) { if (!allowedActions.includes(pullmergeRequestAction)) {
@@ -183,6 +177,8 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
await prisma.build.create({ await prisma.build.create({
data: { data: {
id: buildId, id: buildId,
pullmergeRequestId,
sourceBranch,
applicationId: applicationFound.id, applicationId: applicationFound.id,
destinationDockerId: applicationFound.destinationDocker.id, destinationDockerId: applicationFound.destinationDocker.id,
gitSourceId: applicationFound.gitSource.id, gitSourceId: applicationFound.gitSource.id,
@@ -192,14 +188,7 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
type: 'webhook_pr' type: 'webhook_pr'
} }
}); });
scheduler.workers.get('deployApplication').postMessage({
build_id: buildId,
type: 'webhook_pr',
...applicationFound,
sourceBranch,
pullmergeRequestId
});
return { return {
message: 'Queued. Thank you!' message: 'Queued. Thank you!'
}; };

View File

@@ -89,12 +89,6 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
} }
}); });
scheduler.workers.get('deployApplication').postMessage({
build_id: buildId,
type: 'webhook_commit',
...applicationFound
});
return { return {
message: 'Queued. Thank you!' message: 'Queued. Thank you!'
}; };
@@ -141,6 +135,8 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
await prisma.build.create({ await prisma.build.create({
data: { data: {
id: buildId, id: buildId,
pullmergeRequestId,
sourceBranch,
applicationId: applicationFound.id, applicationId: applicationFound.id,
destinationDockerId: applicationFound.destinationDocker.id, destinationDockerId: applicationFound.destinationDocker.id,
gitSourceId: applicationFound.gitSource.id, gitSourceId: applicationFound.gitSource.id,
@@ -150,14 +146,6 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
type: 'webhook_mr' type: 'webhook_mr'
} }
}); });
scheduler.workers.get('deployApplication').postMessage({
build_id: buildId,
type: 'webhook_mr',
...applicationFound,
sourceBranch,
pullmergeRequestId
});
return { return {
message: 'Queued. Thank you!' message: 'Queued. Thank you!'
}; };

View File

@@ -484,7 +484,6 @@ export async function traefikOtherConfiguration(request: FastifyRequest<TraefikO
} }
throw { status: 500 } throw { status: 500 }
} catch ({ status, message }) { } catch ({ status, message }) {
console.log(status, message);
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }

View File

@@ -14,20 +14,20 @@
"format": "prettier --write --plugin-search-dir=. ." "format": "prettier --write --plugin-search-dir=. ."
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "1.24.2", "@playwright/test": "1.25.1",
"@sveltejs/kit": "1.0.0-next.405", "@sveltejs/kit": "1.0.0-next.405",
"@types/js-cookie": "3.0.2", "@types/js-cookie": "3.0.2",
"@typescript-eslint/eslint-plugin": "5.33.0", "@typescript-eslint/eslint-plugin": "5.35.1",
"@typescript-eslint/parser": "5.33.0", "@typescript-eslint/parser": "5.35.1",
"autoprefixer": "10.4.8", "autoprefixer": "10.4.8",
"eslint": "8.21.0", "eslint": "8.22.0",
"eslint-config-prettier": "8.5.0", "eslint-config-prettier": "8.5.0",
"eslint-plugin-svelte3": "4.0.0", "eslint-plugin-svelte3": "4.0.0",
"postcss": "8.4.16", "postcss": "8.4.16",
"prettier": "2.7.1", "prettier": "2.7.1",
"prettier-plugin-svelte": "2.7.0", "prettier-plugin-svelte": "2.7.0",
"svelte": "3.49.0", "svelte": "3.49.0",
"svelte-check": "2.8.0", "svelte-check": "2.8.1",
"svelte-preprocess": "4.10.7", "svelte-preprocess": "4.10.7",
"tailwindcss": "3.1.8", "tailwindcss": "3.1.8",
"tailwindcss-scrollbar": "0.1.0", "tailwindcss-scrollbar": "0.1.0",
@@ -38,8 +38,9 @@
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@sveltejs/adapter-static": "1.0.0-next.39", "@sveltejs/adapter-static": "1.0.0-next.39",
"@tailwindcss/typography": "^0.5.4",
"cuid": "2.1.8", "cuid": "2.1.8",
"daisyui": "2.22.0", "daisyui": "2.24.0",
"js-cookie": "3.0.1", "js-cookie": "3.0.1",
"p-limit": "4.0.0", "p-limit": "4.0.0",
"svelte-select": "4.4.7", "svelte-select": "4.4.7",

View File

@@ -158,7 +158,7 @@ export const supportedServiceTypesAndVersions = [
ports: { ports: {
main: 80 main: 80
} }
} },
// { // {
// name: 'moodle', // name: 'moodle',
// fancyName: 'Moodle', // fancyName: 'Moodle',
@@ -170,6 +170,28 @@ export const supportedServiceTypesAndVersions = [
// main: 8080 // main: 8080
// } // }
// } // }
{
name: 'glitchTip',
fancyName: 'GlitchTip',
baseImage: 'glitchtip/glitchtip',
images: ['postgres:14-alpine', 'redis:7-alpine'],
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 8000
}
},
{
name: 'searxng',
fancyName: 'SearXNG',
baseImage: 'searxng/searxng',
images: ['redis:6.2-alpine'],
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 8080
}
},
]; ];
export const asyncSleep = (delay: number) => export const asyncSleep = (delay: number) =>

View File

@@ -20,11 +20,12 @@
let usageInterval: any; let usageInterval: any;
let loading = { let loading = {
usage: false, usage: false,
cleanup: false cleanup: false,
restart: false
}; };
import { appSession } from '$lib/store'; import { addToast, appSession } from '$lib/store';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { get } from '$lib/api'; import { get, post } from '$lib/api';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
async function getStatus() { async function getStatus() {
if (loading.usage) return; if (loading.usage) return;
@@ -33,6 +34,25 @@
usage = data.usage; usage = data.usage;
loading.usage = false; loading.usage = false;
} }
async function restartCoolify() {
const sure = confirm(
'Are you sure you would like to restart Coolify? Currently running deployments will be stopped and restarted.'
);
if (sure) {
loading.restart = true;
try {
await post(`/internal/restart`, {});
addToast({
type: 'success',
message: 'Coolify restarted successfully. It will take a moment.'
});
} catch (error) {
return errorNotification(error);
} finally {
loading.restart = false;
}
}
}
onDestroy(() => { onDestroy(() => {
clearInterval(usageInterval); clearInterval(usageInterval);
}); });
@@ -48,65 +68,104 @@
return errorNotification(error); return errorNotification(error);
} }
}); });
async function manuallyCleanupStorage() {
try {
loading.cleanup = true;
await post('/internal/cleanup', {});
return addToast({
message: 'Cleanup done.',
type: 'success'
});
} catch (error) {
return errorNotification(error);
} finally {
loading.cleanup = false;
}
}
</script> </script>
<div class="pb-4"> <div class="w-full">
<div class="title">Hardware Details</div> <div class="flex items-center">
<div class="text-center p-8 "> <h1 class="title lg:text-3xl">Hardware Details</h1>
<div> <div class="flex space-x-4">
<div class="stat w-64"> {#if $appSession.teamId === '0'}
<button on:click={manuallyCleanupStorage} class:loading={loading.cleanup} class="btn btn-sm"
>Cleanup Storage</button
>
<button
on:click={restartCoolify}
class:loading={loading.restart}
class="btn btn-sm bg-red-600 hover:bg-red-500">Restart Coolify</button
>
{/if}
</div>
</div>
<div class="divider" />
<div class="grid grid-flow-col gap-4 grid-rows-3 justify-start lg:justify-center lg:grid-rows-1">
<div class="stats stats-vertical min-w-[16rem] mb-5 rounded bg-transparent">
<div class="stat">
<div class="stat-title">Total Memory</div> <div class="stat-title">Total Memory</div>
<div class="stat-value"> <div class="stat-value text-2xl">
{(usage?.memory.totalMemMb).toFixed(0)}<span class="text-sm">MB</span> {(usage?.memory.totalMemMb).toFixed(0)}<span class="text-sm">MB</span>
</div> </div>
</div> </div>
<div class="stat w-64">
<div class="stat">
<div class="stat-title">Used Memory</div> <div class="stat-title">Used Memory</div>
<div class="stat-value"> <div class="stat-value text-2xl">
{(usage?.memory.usedMemMb).toFixed(0)}<span class="text-sm">MB</span> {(usage?.memory.usedMemMb).toFixed(0)}<span class="text-sm">MB</span>
</div> </div>
</div> </div>
<div class="stat w-64">
<div class="stat">
<div class="stat-title">Free Memory</div> <div class="stat-title">Free Memory</div>
<div class="stat-value"> <div class="stat-value text-2xl">
{usage?.memory.freeMemPercentage}<span class="text-sm">%</span> {usage?.memory.freeMemPercentage}<span class="text-sm">%</span>
</div> </div>
</div> </div>
</div> </div>
<div class="py-10">
<div class="stat w-64"> <div class="stats stats-vertical min-w-[20rem] mb-5 bg-transparent rounded">
<div class="stat-title">Total CPUs</div>
<div class="stat-value"> <div class="stat">
<div class="stat-title">Total CPU</div>
<div class="stat-value text-2xl">
{usage?.cpu.count} {usage?.cpu.count}
</div> </div>
</div> </div>
<div class="stat w-64">
<div class="stat">
<div class="stat-title">CPU Usage</div> <div class="stat-title">CPU Usage</div>
<div class="stat-value"> <div class="stat-value text-2xl">
{usage?.cpu.usage}<span class="text-sm">%</span> {usage?.cpu.usage}<span class="text-sm">%</span>
</div> </div>
</div> </div>
<div class="stat w-64">
<div class="stat">
<div class="stat-title">Load Average (5,10,30mins)</div> <div class="stat-title">Load Average (5,10,30mins)</div>
<div class="stat-value">{usage?.cpu.load}</div> <div class="stat-value text-2xl">{usage?.cpu.load}</div>
</div> </div>
</div> </div>
<div> <div class="stats stats-vertical min-w-[16rem] mb-5 bg-transparent rounded">
<div class="stat w-64"> <div class="stat">
<div class="stat-title">Total Disk</div> <div class="stat-title">Total Disk</div>
<div class="stat-value"> <div class="stat-value text-2xl">
{usage?.disk.totalGb}<span class="text-sm">GB</span> {usage?.disk.totalGb}<span class="text-sm">GB</span>
</div> </div>
</div> </div>
<div class="stat w-64">
<div class="stat">
<div class="stat-title">Used Disk</div> <div class="stat-title">Used Disk</div>
<div class="stat-value"> <div class="stat-value text-2xl">
{usage?.disk.usedGb}<span class="text-sm">GB</span> {usage?.disk.usedGb}<span class="text-sm">GB</span>
</div> </div>
</div> </div>
<div class="stat w-64">
<div class="stat">
<div class="stat-title">Free Disk</div> <div class="stat-title">Free Disk</div>
<div class="stat-value">{usage?.disk.freePercentage}<span class="text-sm">%</span></div> <div class="stat-value text-2xl">
{usage?.disk.freePercentage}<span class="text-sm">%</span>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -2,79 +2,8 @@
export let isAbsolute = true; export let isAbsolute = true;
</script> </script>
<svg
viewBox="0 0 128 128" <svg viewBox="0 0 128 128" class={isAbsolute ? 'absolute top-0 left-0 -m-5 h-12 w-12' : 'mx-auto w-10 h-10'}>
class={isAbsolute ? 'absolute top-0 left-0 -m-8 h-16 w-16' : 'mx-auto w-8 h-8'} <path d="M124.8 52.1c-4.3-2.5-10-2.8-14.8-1.4-.6-5.2-4-9.7-8-12.9l-1.6-1.3-1.4 1.6c-2.7 3.1-3.5 8.3-3.1 12.3.3 2.9 1.2 5.9 3 8.3-1.4.8-2.9 1.9-4.3 2.4-2.8 1-5.9 2-8.9 2H79V49H66V24H51v12H26v13H13v14H1.8l-.2 1.5c-.5 6.4.3 12.6 3 18.5l1.1 2.2.1.2c7.9 13.4 21.7 19 36.8 19 29.2 0 53.3-13.1 64.3-40.6 7.4.4 15-1.8 18.6-8.9l.9-1.8-1.6-1zM28 39h10v11H28V39zm13.1 44.2c0 1.7-1.4 3.1-3.1 3.1-1.7 0-3.1-1.4-3.1-3.1 0-1.7 1.4-3.1 3.1-3.1 1.7.1 3.1 1.4 3.1 3.1zM28 52h10v11H28V52zm-13 0h11v11H15V52zm27.7 50.2c-15.8-.1-24.3-5.4-31.3-12.4 2.1.1 4.1.2 5.9.2 1.6 0 3.2 0 4.7-.1 3.9-.2 7.3-.7 10.1-1.5 2.3 5.3 6.5 10.2 14 13.8h-3.4zM51 63H40V52h11v11zm0-13H40V39h11v11zm13 13H53V52h11v11zm0-13H53V39h11v11zm0-13H53V26h11v11zm13 26H66V52h11v11zM38.8 81.2c-.2-.1-.5-.2-.8-.2-1.2 0-2.2 1-2.2 2.2 0 1.2 1 2.2 2.2 2.2s2.2-1 2.2-2.2c0-.3-.1-.6-.2-.8-.2.3-.4.5-.8.5-.5 0-.9-.4-.9-.9.1-.4.3-.7.5-.8z" fill="#019BC6"></path>
> </svg>
<g
><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#3A4D54"
d="M73.8 50.8h11.3v11.5h5.7c2.6 0 5.3-.5 7.8-1.3 1.2-.4 2.6-1 3.8-1.7-1.6-2.1-2.4-4.7-2.6-7.3-.3-3.5.4-8.1 2.8-10.8l1.2-1.4 1.4 1.1c3.6 2.9 6.5 6.8 7.1 11.4 4.3-1.3 9.3-1 13.1 1.2l1.5.9-.8 1.6c-3.2 6.2-9.9 8.2-16.4 7.8-9.8 24.3-31 35.8-56.8 35.8-13.3 0-25.5-5-32.5-16.8l-.1-.2-1-2.1c-2.4-5.2-3.1-10.9-2.6-16.6l.2-1.7h9.6v-11.4h11.3v-11.2h22.5v-11.3h13.5v22.5z"
/><path
fill="#00AADA"
d="M110.4 55.1c.8-5.9-3.6-10.5-6.4-12.7-3.1 3.6-3.6 13.2 1.3 17.2-2.8 2.4-8.5 4.7-14.5 4.7h-72.2c-.6 6.2.5 11.9 3 16.8l.8 1.5c.5.9 1.1 1.7 1.7 2.6 3 .2 5.7.3 8.2.2 4.9-.1 8.9-.7 12-1.7.5-.2.9.1 1.1.5.2.5-.1.9-.5 1.1-.4.1-.8.3-1.3.4-2.4.7-5 1.1-8.3 1.3h-.6000000000000001c-1.3.1-2.7.1-4.2.1-1.6 0-3.1 0-4.9-.1 6 6.8 15.4 10.8 27.2 10.8 25 0 46.2-11.1 55.5-35.9 6.7.7 13.1-1 16-6.7-4.5-2.7-10.5-1.8-13.9-.1z"
/><path
fill="#28B8EB"
d="M110.4 55.1c.8-5.9-3.6-10.5-6.4-12.7-3.1 3.6-3.6 13.2 1.3 17.2-2.8 2.4-8.5 4.7-14.5 4.7h-68c-.3 9.5 3.2 16.7 9.5 21 4.9-.1 8.9-.7 12-1.7.5-.2.9.1 1.1.5.2.5-.1.9-.5 1.1-.4.1-.8.3-1.3.4-2.4.7-5.2 1.2-8.5 1.4l-.1-.1c8.5 4.4 20.8 4.3 35-1.1 15.8-6.1 30.6-17.7 40.9-30.9-.2.1-.4.1-.5.2z"
/><path
fill="#028BB8"
d="M18.7 71.8c.4 3.3 1.4 6.4 2.9 9.3l.8 1.5c.5.9 1.1 1.7 1.7 2.6 3 .2 5.7.3 8.2.2 4.9-.1 8.9-.7 12-1.7.5-.2.9.1 1.1.5.2.5-.1.9-.5 1.1-.4.1-.8.3-1.3.4-2.4.7-5.2 1.2-8.5 1.4h-.4c-1.3.1-2.7.1-4.1.1-1.6 0-3.2 0-4.9-.1 6 6.8 15.5 10.8 27.3 10.8 21.4 0 40-8.1 50.8-26h-85.1v-.1z"
/><path
fill="#019BC6"
d="M23.5 71.8c1.3 5.8 4.3 10.4 8.8 13.5 4.9-.1 8.9-.7 12-1.7.5-.2.9.1 1.1.5.2.5-.1.9-.5 1.1-.4.1-.8.3-1.3.4-2.4.7-5.2 1.2-8.6 1.4 8.5 4.4 20.8 4.3 34.9-1.1 8.5-3.3 16.8-8.2 24.2-14.1h-70.6z"
/><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#00ACD3"
d="M28.4 52.7h9.8v9.8h-9.8v-9.8zm.8.8h.8v8.1h-.8v-8.1zm1.4 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zM39.6 41.5h9.8v9.8h-9.8v-9.8zm.9.8h.8v8.1h-.8v-8.1zm1.4 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.4 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1z"
/><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#23C2EE"
d="M39.6 52.7h9.8v9.8h-9.8v-9.8zm.9.8h.8v8.1h-.8v-8.1zm1.4 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.4 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1z"
/><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#00ACD3"
d="M50.9 52.7h9.8v9.8h-9.8v-9.8zm.8.8h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.4 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1z"
/><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#23C2EE"
d="M50.9 41.5h9.8v9.8h-9.8v-9.8zm.8.8h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.4 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zM62.2 52.7h9.8v9.8h-9.8v-9.8zm.8.8h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.4 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1z"
/><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#00ACD3"
d="M62.2 41.5h9.8v9.8h-9.8v-9.8zm.8.8h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.4 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1z"
/><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#23C2EE"
d="M62.2 30.2h9.8v9.8h-9.8v-9.8zm.8.8h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.4 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1z"
/><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#00ACD3"
d="M73.5 52.7h9.8v9.8h-9.8v-9.8zm.8.8h.8v8.1h-.8v-8.1zm1.4 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1z"
/><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#D4EEF1"
d="M48.8 78.3c1.5 0 2.7 1.2 2.7 2.7 0 1.5-1.2 2.7-2.7 2.7-1.5 0-2.7-1.2-2.7-2.7 0-1.5 1.2-2.7 2.7-2.7"
/><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#3A4D54"
d="M48.8 79.1c.2 0 .5 0 .7.1-.2.1-.4.4-.4.7 0 .4.4.8.8.8.3 0 .6-.2.7-.4.1.2.1.5.1.7 0 1.1-.9 1.9-1.9 1.9-1.1 0-1.9-.9-1.9-1.9 0-1 .8-1.9 1.9-1.9M1.1 72.8h125.4c-2.7-.7-8.6-1.6-7.7-5.2-5 5.7-16.9 4-20 1.2-3.4 4.9-23 3-24.3-.8-4.2 5-17.3 5-21.5 0-1.4 3.8-21 5.7-24.3.8-3 2.8-15 4.5-20-1.2 1.1 3.5-4.9 4.5-7.6 5.2"
/><path
fill="#BFDBE0"
d="M56 97.8c-6.7-3.2-10.3-7.5-12.4-12.2-2.5.7-5.5 1.2-8.9 1.4-1.3.1-2.7.1-4.1.1-1.7 0-3.4 0-5.2-.1 6 6 13.6 10.7 27.5 10.8h3.1z"
/><path
fill="#D4EEF1"
d="M46.1 89.9c-.9-1.3-1.8-2.8-2.5-4.3-2.5.7-5.5 1.2-8.9 1.4 2.3 1.2 5.7 2.4 11.4 2.9z"
/></g
>
</svg>

View File

@@ -4,7 +4,7 @@
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class={isAbsolute ? 'w-14 h-14 absolute top-0 left-0 -m-5' : 'w-9 mx-auto'} class={isAbsolute ? 'w-14 h-14 absolute top-0 left-0 -m-5' : 'w-9 h-9 mx-auto'}
viewBox="0 0 384 384" viewBox="0 0 384 384"
fill="none" fill="none"
> >

View File

@@ -5,7 +5,7 @@
<svg <svg
viewBox="0 0 700 240" viewBox="0 0 700 240"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class={isAbsolute ? 'w-36 absolute top-0 left-0 -m-3 -mt-5' : 'w-28 mx-auto'} class={isAbsolute ? 'w-36 absolute top-0 left-0 -m-3 -mt-5' : 'w-28 h-28 mx-auto'}
><path fill="#FDBC3D" d="m90.694 107.498-.981.39-20.608 8.23 6.332 6.547z" /><path ><path fill="#FDBC3D" d="m90.694 107.498-.981.39-20.608 8.23 6.332 6.547z" /><path
fill="#8EC63F" fill="#8EC63F"
d="M61.139 77.914 46.632 93 56.9 103.547c8.649-7.169 17.832-10.502 18.653-10.789L61.139 77.914z" d="M61.139 77.914 46.632 93 56.9 103.547c8.649-7.169 17.832-10.502 18.653-10.789L61.139 77.914z"

View File

@@ -4,6 +4,6 @@
<img <img
alt="ghost logo" alt="ghost logo"
class={isAbsolute ? 'w-12 absolute top-0 left-0 -m-3 -mt-5' : 'w-8 mx-auto'} class={isAbsolute ? 'w-12 h-12 absolute top-0 left-0 -m-3 -mt-5' : 'w-8 h-8 mx-auto'}
src="/ghost.png" src="/ghost.png"
/> />

View File

@@ -0,0 +1,51 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<svg
class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 h-8 mx-auto'}
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
style="isolation:isolate"
viewBox="0 0 400 400"
>
<defs>
<clipPath id="_clipPath_5kOQy2sGcuF9aeG3NHWmCAGgMEPQrnNW">
<rect width="400" height="400" />
</clipPath>
</defs>
<g clip-path="url(#_clipPath_5kOQy2sGcuF9aeG3NHWmCAGgMEPQrnNW)">
<g>
<g>
<path
d=" M 276.155 367.684 L 337.655 367.684 L 337.655 180.781 L 205.525 180.781 L 205.525 241.801 L 267.987 241.801 L 267.987 258.617 C 267.987 291.29 238.678 308.586 202.162 308.586 C 156.998 308.586 127.689 282.641 127.689 226.906 L 127.689 173.094 C 127.689 117.359 156.998 91.414 202.162 91.414 C 241.08 91.414 261.74 112.554 271.83 138.5 L 331.409 104.386 C 306.424 52.976 261.74 26.55 202.162 26.55 C 111.353 26.55 50.333 88.531 50.333 201.441 C 50.333 313.872 110.873 373.45 187.748 373.45 C 238.197 373.45 268.947 347.985 273.752 314.352 L 276.155 314.352 L 276.155 367.684 Z "
fill="rgb(132,24,128)"
/>
</g>
<g opacity="0.5">
<path
d=" M 139.701 175.78 L 139.701 173.094 C 139.701 117.359 169.01 91.414 214.174 91.414 C 253.092 91.414 273.752 112.554 283.842 138.5 L 343.421 104.386 C 318.436 52.976 273.752 26.55 214.174 26.55 C 128.962 26.55 69.981 81.125 63.033 181.145 L 139.701 175.78 Z "
fill-rule="evenodd"
fill="rgb(233,64,86)"
/>
</g>
<g opacity="0.5">
<path
d=" M 349.667 305.194 L 349.667 247.137 L 279.998 252.019 L 279.998 258.617 C 279.998 291.29 250.69 308.586 214.174 308.586 C 179.697 308.586 154.459 293.467 144.446 261.518 L 70.341 266.711 C 76.285 288.796 85.348 307.563 96.86 322.909 L 349.667 305.194 Z "
fill-rule="evenodd"
fill="rgb(233,64,86)"
/>
</g>
<path
d=" M 337.655 247.03 L 337.655 180.781 L 205.525 180.781 L 205.525 241.801 L 267.987 241.801 L 267.987 251.912 L 337.655 247.03 Z M 132.401 261.413 C 129.319 251.534 127.689 240.048 127.689 226.906 L 127.689 175.099 L 51.069 180.468 C 50.581 187.25 50.333 194.242 50.333 201.441 C 50.333 225.632 53.136 247.376 58.301 266.606 L 132.401 261.413 Z "
fill-rule="evenodd"
fill="rgb(233,64,86)"
/>
<path
d=" M 337.655 305.862 L 337.655 367.684 L 276.155 367.684 L 276.155 314.352 L 273.752 314.352 C 268.947 347.985 238.197 373.45 187.748 373.45 C 146.712 373.45 110.33 356.473 85.327 323.543 L 337.655 305.862 Z "
fill-rule="evenodd"
fill="rgb(255,63,42)"
/>
</g>
</g>
</svg>

View File

@@ -3,7 +3,7 @@
</script> </script>
<svg <svg
class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 mx-auto'} class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 h-8 mx-auto'}
viewBox="0 0 81 84" viewBox="0 0 81 84"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@@ -4,7 +4,7 @@
<svg <svg
xmlns="http://www.w3.org/2000/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'} class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 h-8 mx-auto'}
fill="none" fill="none"
viewBox="0 0 140 140" viewBox="0 0 140 140"
data-lt-extension-installed="true" data-lt-extension-installed="true"

View File

@@ -4,7 +4,7 @@
<svg <svg
viewBox="0 0 127 74" viewBox="0 0 127 74"
class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 mx-auto'} class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 h-8mx-auto'}
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
><path ><path
d="M.825 73.993l23.244-59.47A21.85 21.85 0 0144.42.625h14.014L35.19 60.096a21.85 21.85 0 01-20.352 13.897H.825z" d="M.825 73.993l23.244-59.47A21.85 21.85 0 0144.42.625h14.014L35.19 60.096a21.85 21.85 0 01-20.352 13.897H.825z"

View File

@@ -4,6 +4,6 @@
<img <img
alt="minio logo" alt="minio logo"
class={isAbsolute ? 'w-7 absolute top-0 left-0 -m-3 -mt-5' : 'w-4 mx-auto'} class={isAbsolute ? 'w-7 h-12 absolute top-0 left-0 -m-3 -mt-5' : 'w-4 h-8 mx-auto'}
src="/minio.png" src="/minio.png"
/> />

View File

@@ -3,6 +3,6 @@
</script> </script>
<img <img
alt="moodle logo" alt="moodle logo"
class={isAbsolute ? 'w-9 absolute top-0 left-0 -m-3 -mt-5' : 'w-8 mx-auto'} class={isAbsolute ? 'w-9 h-9 absolute top-0 left-0 -m-3 -mt-5' : 'w-8 h-8 mx-auto'}
src="/moodle.png" src="/moodle.png"
/> />

View File

@@ -3,7 +3,7 @@
</script> </script>
<svg <svg
class={isAbsolute ? 'w-12 h-12 absolute top-0 left-0 -m-5' : 'w-8 mx-auto'} class={isAbsolute ? 'w-12 h-12 absolute top-0 left-0 -m-5' : 'w-8 h-8 mx-auto'}
viewBox="0 0 220 105" viewBox="0 0 220 105"
> >
<g> <g>

View File

@@ -4,6 +4,6 @@
<img <img
alt="nocodb logo" alt="nocodb logo"
class={isAbsolute ? 'w-10 absolute top-0 left-0 -m-5' : 'w-8 mx-auto'} class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 h-8 mx-auto'}
src="/nocodb.png" src="/nocodb.png"
/> />

View File

@@ -4,6 +4,6 @@
<img <img
alt="plausible logo" alt="plausible logo"
class={isAbsolute ? 'w-9 absolute top-0 left-0 -m-4' : 'w-6 mx-auto'} class={isAbsolute ? 'w-9 h-12 absolute top-0 left-0 -m-4' : 'w-6 h-8 mx-auto'}
src="/plausible.png" src="/plausible.png"
/> />

View File

@@ -0,0 +1,56 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<svg
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg8"
version="1.1"
viewBox="0 0 92 92"
class={isAbsolute ? 'w-12 h-12 absolute top-0 left-0 -m-3 -mt-5' : 'w-8 h-8 mx-auto'}
>
<defs id="defs2" />
<metadata id="metadata5">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g transform="translate(-40.921303,-17.416526)" id="layer1">
<circle
r="0"
style="fill:none;stroke:#000000;stroke-width:12;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
cy="92"
cx="75"
id="path3713"
/>
<circle
r="30"
cy="53.902557"
cx="75.921303"
id="path834"
style="fill:none;fill-opacity:1;stroke:#3050ff;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
/>
<path
d="m 67.514849,37.91524 a 18,18 0 0 1 21.051475,3.312407 18,18 0 0 1 3.137312,21.078282"
id="path852"
style="fill:none;fill-opacity:1;stroke:#3050ff;stroke-width:5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
/>
<rect
transform="rotate(-46.234709)"
ry="1.8669105e-13"
y="122.08995"
x="3.7063529"
height="39.963303"
width="18.846331"
id="rect912"
style="opacity:1;fill:#3050ff;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
/>
</g>
</svg>

View File

@@ -36,4 +36,8 @@
<Icons.Appwrite {isAbsolute} /> <Icons.Appwrite {isAbsolute} />
{:else if type === 'moodle'} {:else if type === 'moodle'}
<Icons.Moodle {isAbsolute} /> <Icons.Moodle {isAbsolute} />
{:else if type === 'glitchTip'}
<Icons.GlitchTip {isAbsolute} />
{:else if type === 'searxng'}
<Icons.Searxng {isAbsolute} />
{/if} {/if}

View File

@@ -7,7 +7,7 @@
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 856.000000 856.000000" viewBox="0 0 856.000000 856.000000"
preserveAspectRatio="xMidYMid meet" preserveAspectRatio="xMidYMid meet"
class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 mx-auto'} class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 h-8 mx-auto'}
> >
<metadata> Created by potrace 1.11, written by Peter Selinger 2001-2013 </metadata> <metadata> Created by potrace 1.11, written by Peter Selinger 2001-2013 </metadata>
<g <g

View File

@@ -3,7 +3,7 @@
</script> </script>
<svg <svg
class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 mx-auto'} class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 h-8mx-auto'}
viewBox="0 0 128 128" viewBox="0 0 128 128"
> >
<path <path

View File

@@ -3,7 +3,7 @@
</script> </script>
<svg <svg
class={isAbsolute ? 'w-10 absolute top-0 left-0 -m-5' : 'w-8 mx-auto'} class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 h-8mx-auto'}
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1" version="1.1"

View File

@@ -2,7 +2,7 @@
export let isAbsolute = false; export let isAbsolute = false;
</script> </script>
<svg class={isAbsolute ? 'w-10 absolute top-0 left-0 -m-5' : 'w-8 mx-auto'} viewBox="0 0 128 128"> <svg class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 h-8mx-auto'} viewBox="0 0 128 128">
<path <path
fill-rule="evenodd" fill-rule="evenodd"
clip-rule="evenodd" clip-rule="evenodd"

View File

@@ -15,4 +15,5 @@ export { default as Hasura } from './Hasura.svelte';
export { default as Fider } from './Fider.svelte'; export { default as Fider } from './Fider.svelte';
export { default as Appwrite } from './Appwrite.svelte'; export { default as Appwrite } from './Appwrite.svelte';
export { default as Moodle } from './Moodle.svelte'; export { default as Moodle } from './Moodle.svelte';
export { default as GlitchTip } from './GlitchTip.svelte';
export { default as Searxng } from './Searxng.svelte';

View File

@@ -159,7 +159,7 @@
"storage_saved": "Storage saved.", "storage_saved": "Storage saved.",
"storage_updated": "Storage updated.", "storage_updated": "Storage updated.",
"storage_deleted": "Storage deleted.", "storage_deleted": "Storage deleted.",
"persistent_storage_explainer": "You can specify any folder that you want to be persistent across deployments. <br>This is useful for storing data such as a database (SQLite) or a cache." "persistent_storage_explainer": "You can specify any folder that you want to be persistent across deployments.<br><span class='text-green-500 font-bold'>/example</span> means it will preserve <span class='text-green-500 font-bold'>/app/example</span> in the container as <span class='text-green-500 font-bold'>/app</span> is <span class='text-green-500 font-bold'>the root directory</span> for your application.<br><br>This is useful for storing data such as a <span class='text-green-500 font-bold'>database (SQLite)</span> or a <span class='text-green-500 font-bold'>cache</span>."
}, },
"deployment_queued": "Deployment queued.", "deployment_queued": "Deployment queued.",
"confirm_to_delete": "Are you sure you would like to delete '{{name}}'?", "confirm_to_delete": "Are you sure you would like to delete '{{name}}'?",

View File

@@ -90,7 +90,7 @@ export const setLocation = (resource: any, settings?: any) => {
return location.set(resource.fqdn) return location.set(resource.fqdn)
} else { } else {
location.set(null); location.set(null);
disabledButton.set(true); disabledButton.set(false);
} }
} }

View File

@@ -120,7 +120,13 @@
<nav class="nav-main"> <nav class="nav-main">
<div class="flex h-screen w-full flex-col items-center transition-all duration-100"> <div class="flex h-screen w-full flex-col items-center transition-all duration-100">
{#if !$appSession.whiteLabeled} {#if !$appSession.whiteLabeled}
<div class="my-4 h-10 w-10"><img src="/favicon.png" alt="coolLabs logo" /></div> <div class="mb-2 mt-4 h-10 w-10">
<img src="/favicon.png" alt="coolLabs logo" />
</div>
{:else if $appSession.whiteLabeledDetails.icon}
<div class="mb-2 mt-4 h-10 w-10">
<img src={$appSession.whiteLabeledDetails.icon} alt="White labeled logo" />
</div>
{/if} {/if}
<div class="flex flex-col space-y-2 py-2" class:mt-2={$appSession.whiteLabeled}> <div class="flex flex-col space-y-2 py-2" class:mt-2={$appSession.whiteLabeled}>
<a <a

View File

@@ -57,11 +57,12 @@
message: 'Secret added.', message: 'Secret added.',
type: 'success' type: 'success'
}); });
} } else {
addToast({ addToast({
message: 'Secret updated.', message: 'Secret updated.',
type: 'success' type: 'success'
}); });
}
dispatch('refresh'); dispatch('refresh');
} catch (error) { } catch (error) {
console.log(error); console.log(error);

View File

@@ -66,7 +66,6 @@
let loading = false; let loading = false;
let statusInterval: any; let statusInterval: any;
let isQueueActive = false;
$disabledButton = $disabledButton =
!$appSession.isAdmin || !$appSession.isAdmin ||
(!application.fqdn && !application.settings.isBot) || (!application.fqdn && !application.settings.isBot) ||
@@ -77,9 +76,9 @@
const { id } = $page.params; const { id } = $page.params;
async function handleDeploySubmit() { async function handleDeploySubmit(forceRebuild = false) {
try { try {
const { buildId } = await post(`/applications/${id}/deploy`, { ...application }); const { buildId } = await post(`/applications/${id}/deploy`, { ...application, forceRebuild });
addToast({ addToast({
message: $t('application.deployment_queued'), message: $t('application.deployment_queued'),
type: 'success' type: 'success'
@@ -121,7 +120,6 @@
if ($status.application.loading) return; if ($status.application.loading) return;
$status.application.loading = true; $status.application.loading = true;
const data = await get(`/applications/${id}/status`); const data = await get(`/applications/${id}/status`);
isQueueActive = data.isQueueActive;
$status.application.isRunning = data.isRunning; $status.application.isRunning = data.isRunning;
$status.application.isExited = data.isExited; $status.application.isExited = data.isExited;
$status.application.loading = false; $status.application.loading = false;
@@ -141,8 +139,7 @@
if ( if (
application.gitSourceId && application.gitSourceId &&
application.destinationDockerId && application.destinationDockerId &&
(application.fqdn || (application.fqdn || application.settings.isBot)
application.settings.isBot)
) { ) {
await getStatus(); await getStatus();
statusInterval = setInterval(async () => { statusInterval = setInterval(async () => {
@@ -179,9 +176,10 @@
<polyline points="15 4 20 4 20 9" /> <polyline points="15 4 20 4 20 9" />
</svg></a </svg></a
> >
<div class="border border-coolgray-500 h-8" />
{/if} {/if}
<div class="border border-coolgray-500 h-8" />
{#if $status.application.isExited} {#if $status.application.isExited}
<a <a
href={!$disabledButton ? `/applications/${id}/logs` : null} href={!$disabledButton ? `/applications/${id}/logs` : null}
@@ -256,16 +254,13 @@
<rect x="14" y="5" width="4" height="14" rx="1" /> <rect x="14" y="5" width="4" height="14" rx="1" />
</svg> </svg>
</button> </button>
<form on:submit|preventDefault={handleDeploySubmit}> <form on:submit|preventDefault={() => handleDeploySubmit(true)}>
<button <button
type="submit" type="submit"
disabled={$disabledButton || !isQueueActive} disabled={$disabledButton}
class:hover:text-green-500={isQueueActive}
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm flex items-center space-x-2" class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm flex items-center space-x-2"
data-tip={$appSession.isAdmin data-tip={$appSession.isAdmin
? isQueueActive ? 'Force Rebuild Application'
? 'Rebuild Application'
: 'Autoupdate inprogress. Cannot rebuild application.'
: 'You do not have permission to rebuild application.'} : 'You do not have permission to rebuild application.'}
> >
<svg <svg
@@ -287,7 +282,7 @@
</button> </button>
</form> </form>
{:else} {:else}
<form on:submit|preventDefault={handleDeploySubmit}> <form on:submit|preventDefault={() => handleDeploySubmit(false)}>
<button <button
type="submit" type="submit"
disabled={$disabledButton} disabled={$disabledButton}
@@ -359,7 +354,7 @@
<button <button
disabled={$disabledButton} disabled={$disabledButton}
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm" class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm"
data-tip="Secret" data-tip="Secrets"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@@ -26,7 +26,7 @@
delete tempBuildPack.color; delete tempBuildPack.color;
delete tempBuildPack.hoverColor; delete tempBuildPack.hoverColor;
if (foundConfig.buildPack !== name) { if (foundConfig?.buildPack !== name) {
await post(`/applications/${id}`, { ...tempBuildPack, buildPack: name }); await post(`/applications/${id}`, { ...tempBuildPack, buildPack: name });
} }
await post(`/applications/${id}/configuration/buildpack`, { buildPack: name }); await post(`/applications/${id}/configuration/buildpack`, { buildPack: name });

View File

@@ -0,0 +1,199 @@
<script lang="ts">
import { get, post } from '$lib/api';
import { t } from '$lib/translations';
import { page } from '$app/stores';
import Select from 'svelte-select';
import Explainer from '$lib/components/Explainer.svelte';
import { goto } from '$app/navigation';
import { errorNotification } from '$lib/common';
const { id } = $page.params;
let publicRepositoryLink: string;
let projectId: number;
let repositoryName: string;
let branchName: string;
let ownerName: string;
let type: string;
let branchSelectOptions: any = [];
let loading = {
branches: false
};
async function loadBranches() {
try {
loading.branches = true;
const protocol = publicRepositoryLink.split(':')[0];
const gitUrl = publicRepositoryLink.replace('http://', '').replace('https://', '');
let [host, ...path] = gitUrl.split('/');
const [owner, repository, ...branch] = path;
ownerName = owner;
repositoryName = repository;
if (host === 'github.com') {
host = 'api.github.com';
type = 'github';
if (branch[0] === 'tree' && branch[1]) {
branchName = branch[1];
}
}
if (host === 'gitlab.com') {
host = 'gitlab.com/api/v4';
type = 'gitlab';
if (branch[1] === 'tree' && branch[2]) {
branchName = branch[2];
}
}
const apiUrl = `${protocol}://${host}`;
if (type === 'github') {
const repositoryDetails = await get(`${apiUrl}/repos/${ownerName}/${repositoryName}`);
projectId = repositoryDetails.id.toString();
}
if (type === 'gitlab') {
const repositoryDetails = await get(`${apiUrl}/projects/${ownerName}%2F${repositoryName}`);
projectId = repositoryDetails.id.toString();
}
if (type === 'github' && branchName) {
try {
await get(`${apiUrl}/repos/${ownerName}/${repositoryName}/branches/${branchName}`);
await saveRepository();
loading.branches = false;
return;
} catch (error) {
errorNotification(error);
}
}
if (type === 'gitlab' && branchName) {
try {
await get(
`${apiUrl}/projects/${ownerName}%2F${repositoryName}/repository/branches/${branchName}`
);
await saveRepository();
loading.branches = false;
return;
} catch (error) {
errorNotification(error);
}
}
let branches: any[] = [];
let page = 1;
let branchCount = 0;
const loadedBranches = await loadBranchesByPage(
apiUrl,
ownerName,
repositoryName,
page,
type
);
branches = branches.concat(loadedBranches);
branchCount = branches.length;
if (branchCount === 100) {
while (branchCount === 100) {
page = page + 1;
const nextBranches = await loadBranchesByPage(
apiUrl,
ownerName,
repositoryName,
page,
type
);
branches = branches.concat(nextBranches);
branchCount = nextBranches.length;
}
}
loading.branches = false;
branchSelectOptions = branches.map((branch: any) => ({
value: branch.name,
label: branch.name
}));
} catch (error) {
return errorNotification(error);
} finally {
loading.branches = false;
}
}
async function loadBranchesByPage(
apiUrl: string,
owner: string,
repository: string,
page = 1,
type: string
) {
if (type === 'github') {
return await get(`${apiUrl}/repos/${owner}/${repository}/branches?per_page=100&page=${page}`);
}
if (type === 'gitlab') {
return await get(
`${apiUrl}/projects/${ownerName}%2F${repositoryName}/repository/branches?page=${page}`
);
}
}
async function saveRepository(event?: any) {
try {
if (event?.detail?.value) {
branchName = event.detail.value;
}
await post(`/applications/${id}/configuration/source`, {
gitSourceId: null,
forPublic: true,
type
});
await post(`/applications/${id}/configuration/repository`, {
repository: `${ownerName}/${repositoryName}`,
branch: branchName,
projectId,
autodeploy: false,
webhookToken: null,
isPublicRepository: true
});
return await goto(`/applications/${id}/configuration/destination`);
} catch (error) {
return errorNotification(error);
}
}
</script>
<div class="mx-auto max-w-5xl">
<div class="grid grid-flow-row gap-2 px-10">
<div class="flex">
<form class="flex" on:submit|preventDefault={loadBranches}>
<div class="space-y-4">
<input
placeholder="eg: https://github.com/coollabsio/nodejs-example/tree/main"
class="text-xs"
bind:value={publicRepositoryLink}
/>
{#if branchSelectOptions.length > 0}
<div class="custom-select-wrapper">
<Select
placeholder={loading.branches
? $t('application.configuration.loading_branches')
: !publicRepositoryLink
? $t('application.configuration.select_a_repository_first')
: $t('application.configuration.select_a_branch')}
isWaiting={loading.branches}
showIndicator={!!publicRepositoryLink && !loading.branches}
id="branches"
on:select={saveRepository}
items={branchSelectOptions}
isDisabled={loading.branches || !!!publicRepositoryLink}
isClearable={false}
/>
</div>
{/if}
</div>
<button class="btn mx-4 bg-orange-600" class:loading={loading.branches} type="submit"
>Load Repository</button
>
</form>
</div>
</div>
<Explainer
text="Examples:<br><br>https://github.com/coollabsio/nodejs-example<br>https://github.com/coollabsio/nodejs-example/tree/main<br>https://gitlab.com/aleveha/fastify-example<br>https://gitlab.com/aleveha/fastify-example/-/tree/master<br><br>Only works with Github.com and Gitlab.com."
/>
</div>

View File

@@ -47,6 +47,7 @@
export let branch: any; export let branch: any;
export let type: any; export let type: any;
export let application: any; export let application: any;
export let isPublicRepository: boolean;
function checkPackageJSONContents({ key, json }: { key: any; json: any }) { function checkPackageJSONContents({ key, json }: { key: any; json: any }) {
return json?.dependencies?.hasOwnProperty(key) || json?.devDependencies?.hasOwnProperty(key); return json?.dependencies?.hasOwnProperty(key) || json?.devDependencies?.hasOwnProperty(key);
@@ -236,7 +237,7 @@
if (error.message === 'Bad credentials') { if (error.message === 'Bad credentials') {
const { token } = await get(`/applications/${id}/configuration/githubToken`); const { token } = await get(`/applications/${id}/configuration/githubToken`);
$appSession.tokens.github = token; $appSession.tokens.github = token;
return await scanRepository() return await scanRepository();
} }
return errorNotification(error); return errorNotification(error);
} finally { } finally {
@@ -245,7 +246,11 @@
} }
} }
onMount(async () => { onMount(async () => {
await scanRepository(); if (!isPublicRepository) {
await scanRepository();
} else {
scanning = false;
}
}); });
</script> </script>
@@ -262,27 +267,25 @@
</div> </div>
</div> </div>
{:else} {:else}
<div class="max-w-5xl mx-auto ">
<div class="title pb-2">Coolify</div>
<div class="max-w-7xl mx-auto ">
<div class="title pb-2">Coolify Buildpacks</div>
<div class="flex flex-wrap justify-center"> <div class="flex flex-wrap justify-center">
{#each buildPacks.filter(bp => bp.isCoolifyBuildPack === true) as buildPack} {#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true) as buildPack}
<div class="p-2"> <div class="p-2">
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig /> <BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
</div> </div>
{/each} {/each}
</div> </div>
</div> </div>
<div class="max-w-7xl mx-auto "> <div class="max-w-5xl mx-auto ">
<div class="title pb-2">Heroku</div> <div class="title pb-2">Other</div>
<div class="flex flex-wrap justify-center"> <div class="flex flex-wrap justify-center">
{#each buildPacks.filter(bp => bp.isHerokuBuildPack === true) as buildPack} {#each buildPacks.filter((bp) => bp.isHerokuBuildPack === true) as buildPack}
<div class="p-2"> <div class="p-2">
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig /> <BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
</div> </div>
{/each} {/each}
</div> </div>
</div> </div>
{/if} {/if}

View File

@@ -48,3 +48,4 @@
<GitlabRepositories {application} {appId} {settings} /> <GitlabRepositories {application} {appId} {settings} />
{/if} {/if}
</div> </div>

View File

@@ -31,6 +31,8 @@
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import { appSession } from '$lib/store'; import { appSession } from '$lib/store';
import PublicRepository from './_PublicRepository.svelte';
import Explainer from '$lib/components/Explainer.svelte';
const { id } = $page.params; const { id } = $page.params;
const from = $page.url.searchParams.get('from'); const from = $page.url.searchParams.get('from');
@@ -71,120 +73,126 @@
{$t('application.configuration.select_a_git_source')} {$t('application.configuration.select_a_git_source')}
</div> </div>
</div> </div>
<div class="flex flex-col justify-center"> <div class="max-w-5xl mx-auto ">
{#if !filteredSources || ownSources.length === 0} <div class="title pb-8">Git App</div>
<div class="flex-col"> <div class="flex flex-wrap justify-center">
<div class="pb-2 text-center font-bold"> {#if !filteredSources || ownSources.length === 0}
{$t('application.configuration.no_configurable_git')} <div class="flex-col">
</div> <div class="pb-2 text-center font-bold">
<div class="flex justify-center"> {$t('application.configuration.no_configurable_git')}
<a </div>
href="/sources/new?from={$page.url.pathname}" <div class="flex justify-center">
class="add-icon bg-orange-600 hover:bg-orange-500" <a
> href="/sources/new?from={$page.url.pathname}"
<svg class="add-icon bg-orange-600 hover:bg-orange-500"
class="w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg
> >
</a> <svg
class="w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg
>
</a>
</div>
</div> </div>
</div> {:else}
{:else} <div class="flex flex-col flex-wrap justify-center px-2 md:flex-row ">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row "> {#each ownSources as source}
{#each ownSources as source} <div class="p-2 relative">
<div class="p-2 relative"> <div class="absolute -m-4">
<div class="absolute -m-4"> {#if source?.type === 'gitlab'}
{#if source?.type === 'gitlab'} <svg viewBox="0 0 128 128" class="w-8">
<svg viewBox="0 0 128 128" class="w-8"> <path
<path fill="#FC6D26"
fill="#FC6D26" d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357" /><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path fill="#FC6D26"
fill="#FC6D26" d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if source?.type === 'github'}
<svg viewBox="0 0 128 128" class="w-8">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path /><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0" fill="#FCA326"
/></g d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
> /><path
</svg> fill="#E24329"
{/if} d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if source?.type === 'github'}
<svg viewBox="0 0 128 128" class="w-8">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</div>
<form on:submit|preventDefault={() => handleSubmit(source.id)}>
<button
disabled={source.gitlabApp && !source.gitlabAppId}
type="submit"
class="disabled:opacity-95 bg-coolgray-200 disabled:text-white box-selection hover:bg-orange-700 group"
class:border-red-500={source.gitlabApp && !source.gitlabAppId}
class:border-0={source.gitlabApp && !source.gitlabAppId}
class:border-l-4={source.gitlabApp && !source.gitlabAppId}
>
<div class="font-bold text-xl text-center truncate">{source.name}</div>
{#if source.gitlabApp && !source.gitlabAppId}
<div class="font-bold text-center truncate text-red-500 group-hover:text-white">
Configuration missing
</div>
{/if}
</button>
</form>
</div> </div>
<form on:submit|preventDefault={() => handleSubmit(source.id)}> {/each}
<button </div>
disabled={source.gitlabApp && !source.gitlabAppId}
type="submit"
class="disabled:opacity-95 bg-coolgray-200 disabled:text-white box-selection hover:bg-orange-700 group"
class:border-red-500={source.gitlabApp && !source.gitlabAppId}
class:border-0={source.gitlabApp && !source.gitlabAppId}
class:border-l-4={source.gitlabApp && !source.gitlabAppId}
>
<div class="font-bold text-xl text-center truncate">{source.name}</div>
{#if source.gitlabApp && !source.gitlabAppId}
<div class="font-bold text-center truncate text-red-500 group-hover:text-white">
Configuration missing
</div>
{/if}
</button>
</form>
</div>
{/each}
</div>
{#if otherSources.length > 0 && $appSession.teamId === '0'} {#if otherSources.length > 0 && $appSession.teamId === '0'}
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Sources</div> <div class="px-6 pb-5 pt-10 text-xl font-bold">Other Sources</div>
{/if}
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each otherSources as source}
<div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(source.id)}>
<button
disabled={source.gitlabApp && !source.gitlabAppId}
type="submit"
class="disabled:opacity-95 bg-coolgray-200 disabled:text-white box-selection hover:bg-orange-700 group"
class:border-red-500={source.gitlabApp && !source.gitlabAppId}
class:border-0={source.gitlabApp && !source.gitlabAppId}
class:border-l-4={source.gitlabApp && !source.gitlabAppId}
>
<div class="font-bold text-xl text-center truncate">{source.name}</div>
{#if source.gitlabApp && !source.gitlabAppId}
<div class="font-bold text-center truncate text-red-500 group-hover:text-white">
{$t('application.configuration.configuration_missing')}
</div>
{/if}
</button>
</form>
</div>
{/each}
</div>
{/if} {/if}
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row"> </div>
{#each otherSources as source} <div class="title py-4">Public Repository</div>
<div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(source.id)}> <PublicRepository />
<button
disabled={source.gitlabApp && !source.gitlabAppId}
type="submit"
class="disabled:opacity-95 bg-coolgray-200 disabled:text-white box-selection hover:bg-orange-700 group"
class:border-red-500={source.gitlabApp && !source.gitlabAppId}
class:border-0={source.gitlabApp && !source.gitlabAppId}
class:border-l-4={source.gitlabApp && !source.gitlabAppId}
>
<div class="font-bold text-xl text-center truncate">{source.name}</div>
{#if source.gitlabApp && !source.gitlabAppId}
<div class="font-bold text-center truncate text-red-500 group-hover:text-white">
{$t('application.configuration.configuration_missing')}
</div>
{/if}
</button>
</form>
</div>
{/each}
</div>
{/if}
</div> </div>

View File

@@ -114,10 +114,33 @@
buildPack: application.buildPack, buildPack: application.buildPack,
deploymentType: application.deploymentType deploymentType: application.deploymentType
}); });
application = { const baseImageCorrect = data.baseImages.filter(
...application, (image: any) => image.value === application.baseImage
...data );
}; if (baseImageCorrect.length === 0) {
application.baseImage = data.baseImage;
}
application.baseImages = data.baseImages;
const baseBuildImageCorrect = data.baseBuildImages.filter(
(image: any) => image.value === application.baseBuildImage
);
if (baseBuildImageCorrect.length === 0) {
application.baseBuildImage = data.baseBuildImage;
}
application.baseBuildImages = data.baseBuildImages;
if (application.deploymentType === 'static' && application.port !== '80') {
application.port = data.port;
}
if (application.deploymentType === 'node' && application.port === '80') {
application.port = data.port;
}
if (application.deploymentType === 'static' && !application.publishDirectory) {
application.publishDirectory = data.publishDirectory;
}
if (application.deploymentType === 'node' && application.publishDirectory === 'out') {
application.publishDirectory = data.publishDirectory;
}
} }
async function changeSettings(name: any) { async function changeSettings(name: any) {
if (name === 'debug') { if (name === 'debug') {
@@ -133,6 +156,7 @@
autodeploy = !autodeploy; autodeploy = !autodeploy;
} }
if (name === 'isBot') { if (name === 'isBot') {
if ($status.application.isRunning) return;
isBot = !isBot; isBot = !isBot;
application.settings.isBot = isBot; application.settings.isBot = isBot;
setLocation(application, settings); setLocation(application, settings);
@@ -345,8 +369,11 @@
<label for="gitSource" class="text-base font-bold text-stone-100" <label for="gitSource" class="text-base font-bold text-stone-100"
>{$t('application.git_source')}</label >{$t('application.git_source')}</label
> >
{#if isDisabled} {#if isDisabled || application.settings.isPublicRepository}
<input disabled={isDisabled} value={application.gitSource.name} /> <input
disabled={isDisabled || application.settings.isPublicRepository}
value={application.gitSource.name}
/>
{:else} {:else}
<a <a
href={`/applications/${id}/configuration/source?from=/applications/${id}`} href={`/applications/${id}/configuration/source?from=/applications/${id}`}
@@ -363,8 +390,11 @@
<label for="repository" class="text-base font-bold text-stone-100" <label for="repository" class="text-base font-bold text-stone-100"
>{$t('application.git_repository')}</label >{$t('application.git_repository')}</label
> >
{#if isDisabled} {#if isDisabled || application.settings.isPublicRepository}
<input disabled={isDisabled} value="{application.repository}/{application.branch}" /> <input
disabled={isDisabled || application.settings.isPublicRepository}
value="{application.repository}/{application.branch}"
/>
{:else} {:else}
<a <a
href={`/applications/${id}/configuration/repository?from=/applications/${id}&to=/applications/${id}/configuration/buildpack`} href={`/applications/${id}/configuration/repository?from=/applications/${id}&to=/applications/${id}/configuration/buildpack`}
@@ -487,7 +517,8 @@
bind:setting={isBot} bind:setting={isBot}
on:click={() => changeSettings('isBot')} on:click={() => changeSettings('isBot')}
title="Is your application a bot?" title="Is your application a bot?"
description="You can deploy applications without domains. <br>They will listen on <span class='text-green-500 font-bold'>IP:EXPOSEDPORT</span> instead.<br></Setting><br>Useful to host <span class='text-green-500 font-bold'>Twitch bots.</span>" description="You can deploy applications without domains. <br>You can also make them to listen on <span class='text-green-500 font-bold'>IP:EXPOSEDPORT</span> as well.<br></Setting><br>Useful to host <span class='text-green-500 font-bold'>Twitch bots, regular jobs, or anything that does not require an incoming connection.</span>"
disabled={$status.application.isRunning}
/> />
</div> </div>
{#if !isBot} {#if !isBot}
@@ -612,7 +643,7 @@
</div> </div>
{/if} {/if}
{/if} {/if}
{#if !staticDeployments.includes(application.buildPack) && !isBot} {#if !staticDeployments.includes(application.buildPack)}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="port" class="text-base font-bold text-stone-100">{$t('forms.port')}</label> <label for="port" class="text-base font-bold text-stone-100">{$t('forms.port')}</label>
<input <input
@@ -623,6 +654,7 @@
bind:value={application.port} bind:value={application.port}
placeholder="{$t('forms.default')}: 'python' ? '8000' : '3000'" placeholder="{$t('forms.default')}: 'python' ? '8000' : '3000'"
/> />
<Explainer text={'The port your application listens on.'} />
</div> </div>
{/if} {/if}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
@@ -633,7 +665,6 @@
name="exposePort" name="exposePort"
id="exposePort" id="exposePort"
bind:value={application.exposePort} bind:value={application.exposePort}
required={isBot}
placeholder="12345" placeholder="12345"
/> />
<Explainer <Explainer
@@ -770,15 +801,17 @@
<div class="title">{$t('application.features')}</div> <div class="title">{$t('application.features')}</div>
</div> </div>
<div class="px-10 pb-10"> <div class="px-10 pb-10">
<div class="grid grid-cols-2 items-center"> {#if !application.settings.isPublicRepository}
<Setting <div class="grid grid-cols-2 items-center">
isCenter={false} <Setting
bind:setting={autodeploy} isCenter={false}
on:click={() => changeSettings('autodeploy')} bind:setting={autodeploy}
title={$t('application.enable_automatic_deployment')} on:click={() => changeSettings('autodeploy')}
description={$t('application.enable_auto_deploy_webhooks')} title={$t('application.enable_automatic_deployment')}
/> description={$t('application.enable_auto_deploy_webhooks')}
</div> />
</div>
{/if}
{#if !application.settings.isBot} {#if !application.settings.isBot}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<Setting <Setting

View File

@@ -109,7 +109,7 @@
<div class="flex justify-end sticky top-0 p-2 mx-1"> <div class="flex justify-end sticky top-0 p-2 mx-1">
<button <button
on:click={followBuild} on:click={followBuild}
class="bg-transparent btn btn-sm tooltip tooltip-primary tooltip-bottom hover:text-green-500 hover:bg-coolgray-500" class="bg-transparent btn btn-sm btn-link tooltip tooltip-primary tooltip-bottom hover:text-green-500 hover:bg-coolgray-500"
data-tip="Follow logs" data-tip="Follow logs"
class:text-green-500={followingBuild} class:text-green-500={followingBuild}
> >

View File

@@ -146,6 +146,7 @@
class="tooltip tooltip-primary tooltip-top flex cursor-pointer items-center justify-center border-l-2 py-4 no-underline transition-all duration-100 hover:bg-coolgray-400 hover:shadow-xl" class="tooltip tooltip-primary tooltip-top flex cursor-pointer items-center justify-center border-l-2 py-4 no-underline transition-all duration-100 hover:bg-coolgray-400 hover:shadow-xl"
class:bg-coolgray-400={buildId === build.id} class:bg-coolgray-400={buildId === build.id}
class:border-red-500={build.status === 'failed'} class:border-red-500={build.status === 'failed'}
class:border-orange-500={build.status === 'canceled'}
class:border-green-500={build.status === 'success'} class:border-green-500={build.status === 'success'}
class:border-yellow-500={build.status === 'running'} class:border-yellow-500={build.status === 'running'}
> >
@@ -157,7 +158,6 @@
{build.type} {build.type}
</div> </div>
</div> </div>
<div class="flex-1" />
<div class="w-48 text-center text-xs"> <div class="w-48 text-center text-xs">
{#if build.status === 'running'} {#if build.status === 'running'}
@@ -177,7 +177,7 @@
{#if !noMoreBuilds} {#if !noMoreBuilds}
{#if buildCount > 5} {#if buildCount > 5}
<div class="flex space-x-2"> <div class="flex space-x-2">
<button disabled={noMoreBuilds} class="w-full" on:click={loadMoreBuilds} <button disabled={noMoreBuilds} class=" btn btn-sm w-full" on:click={loadMoreBuilds}
>{$t('application.build.load_more')}</button >{$t('application.build.load_more')}</button
> >
</div> </div>

View File

@@ -147,7 +147,7 @@
<div class="flex justify-end sticky top-0 p-1 mx-1"> <div class="flex justify-end sticky top-0 p-1 mx-1">
<button <button
on:click={followBuild} on:click={followBuild}
class="bg-transparent btn btn-sm tooltip tooltip-primary tooltip-bottom" class="bg-transparent btn btn-sm btn-link tooltip tooltip-primary tooltip-bottom"
data-tip="Follow logs" data-tip="Follow logs"
class:text-green-500={followingLogs} class:text-green-500={followingLogs}
> >

View File

@@ -194,12 +194,13 @@
</div> </div>
</a> </a>
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<button class="bg-coollabs hover:bg-coollabs-100" on:click={() => redeploy(container)} <button class="btn btn-sm bg-coollabs hover:bg-coollabs-100" on:click={() => redeploy(container)}
>{$t('application.preview.redeploy')}</button >{$t('application.preview.redeploy')}</button
> >
</div> </div>
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<button <button
class="btn btn-sm"
class:bg-red-600={!loading.removing} class:bg-red-600={!loading.removing}
class:hover:bg-red-500={!loading.removing} class:hover:bg-red-500={!loading.removing}
disabled={loading.removing} disabled={loading.removing}

View File

@@ -43,7 +43,8 @@
const batchSecretsPairs = eachValuePair const batchSecretsPairs = eachValuePair
.filter((secret) => !secret.startsWith('#') && secret) .filter((secret) => !secret.startsWith('#') && secret)
.map((secret) => { .map((secret) => {
const [name, value] = secret.split('='); const [name, ...rest] = secret.split('=');
const value = rest.join('=');
const cleanValue = value?.replaceAll('"', '') || ''; const cleanValue = value?.replaceAll('"', '') || '';
return { return {
name, name,

View File

@@ -87,9 +87,7 @@
</div> </div>
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4"> <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={$t('application.storage.persistent_storage_explainer')} />
</div>
<table class="mx-auto border-separate text-left"> <table class="mx-auto border-separate text-left">
<thead> <thead>
<tr class="h-12"> <tr class="h-12">
@@ -109,4 +107,7 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="flex justify-center py-4 text-center">
<Explainer customClass="w-full" text={$t('application.storage.persistent_storage_explainer')} />
</div>
</div> </div>

View File

@@ -64,7 +64,7 @@
</button> </button>
{/if} {/if}
</div> </div>
<div class="flex-col justify-center"> <div class="flex-col justify-center mt-10 pb-12 sm:pb-16">
{#if !applications || ownApplications.length === 0} {#if !applications || ownApplications.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="text-center text-xl font-bold">{$t('application.no_applications_found')}</div> <div class="text-center text-xl font-bold">{$t('application.no_applications_found')}</div>

View File

@@ -61,7 +61,7 @@
</button> </button>
</div> </div>
<div class="flex-col justify-center"> <div class="flex-col justify-center mt-10 pb-12 sm:pb-16">
{#if !databases || ownDatabases.length === 0} {#if !databases || ownDatabases.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="text-center text-xl font-bold">{$t('database.no_databases_found')}</div> <div class="text-center text-xl font-bold">{$t('database.no_databases_found')}</div>

View File

@@ -56,7 +56,7 @@
</a> </a>
{/if} {/if}
</div> </div>
<div class="flex-col justify-center"> <div class="flex-col justify-center mt-10 pb-12 sm:pb-16">
{#if !destinations || ownDestinations.length === 0} {#if !destinations || ownDestinations.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="text-center text-xl font-bold">{$t('destination.no_destination_found')}</div> <div class="text-center text-xl font-bold">{$t('destination.no_destination_found')}</div>

View File

@@ -212,6 +212,7 @@
<div class="flex items-center justify-center pt-3"> <div class="flex items-center justify-center pt-3">
<button <button
on:click|preventDefault={() => switchTeam(team.id)} on:click|preventDefault={() => switchTeam(team.id)}
class="btn btn-sm"
class:bg-fuchsia-600={$appSession.teamId !== team.id} class:bg-fuchsia-600={$appSession.teamId !== team.id}
class:hover:bg-fuchsia-500={$appSession.teamId !== team.id} class:hover:bg-fuchsia-500={$appSession.teamId !== team.id}
class:bg-transparent={$appSession.teamId === team.id} class:bg-transparent={$appSession.teamId === team.id}

View File

@@ -25,18 +25,38 @@
</script> </script>
<script lang="ts"> <script lang="ts">
export let team: any;
export let currentTeam: string;
export let teams: any;
import { page } from '$app/stores'; import { page } from '$app/stores';
import { errorNotification, handlerNotFoundLoad } from '$lib/common'; import { errorNotification, handlerNotFoundLoad } from '$lib/common';
import { appSession } from '$lib/store'; import { appSession } from '$lib/store';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import DeleteIcon from '$lib/components/DeleteIcon.svelte'; import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import Cookies from 'js-cookie';
const { id } = $page.params; const { id } = $page.params;
async function deleteTeam() { async function deleteTeam() {
const sure = confirm('Are you sure you want to delete this team?'); const sure = confirm('Are you sure you want to delete this team?');
if (sure) { if (sure) {
try { try {
await del(`/iam/team/${id}`, { id }); await del(`/iam/team/${id}`, { id });
if (currentTeam === id) {
const switchTeam = teams.find((team: any) => team.id !== id);
const payload = await get(`/user?teamId=${switchTeam.id}`);
if (payload.token) {
Cookies.set('token', payload.token, {
path: '/'
});
$appSession.teamId = payload.teamId;
$appSession.userId = payload.userId;
$appSession.permission = payload.permission;
$appSession.isAdmin = payload.isAdmin;
return window.location.assign('/iam');
}
}
return await goto('/iam', { replaceState: true }); return await goto('/iam', { replaceState: true });
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
@@ -47,16 +67,18 @@
{#if id !== 'new'} {#if id !== 'new'}
<nav class="nav-side"> <nav class="nav-side">
<button {#if team.id !== '0'}
on:click={deleteTeam} <button
type="submit" on:click={deleteTeam}
disabled={!$appSession.isAdmin} type="submit"
class:hover:text-red-500={$appSession.isAdmin} disabled={!$appSession.isAdmin}
class="icons tooltip tooltip-primary tooltip-left bg-transparent text-sm" class:hover:text-red-500={$appSession.isAdmin}
data-tip={$appSession.isAdmin class="icons tooltip tooltip-primary tooltip-left bg-transparent text-sm"
? 'Delete' data-tip={$appSession.isAdmin
: $t('destination.permission_denied_delete_destination')}><DeleteIcon /></button ? 'Delete'
> : $t('destination.permission_denied_delete_destination')}><DeleteIcon /></button
>
{/if}
</nav> </nav>
{/if} {/if}
<slot /> <slot />

View File

@@ -36,10 +36,6 @@
import ServiceIcons from '$lib/components/svg/services/ServiceIcons.svelte'; import ServiceIcons from '$lib/components/svg/services/ServiceIcons.svelte';
import { dev } from '$app/env'; import { dev } from '$app/env';
let loading = {
cleanup: false
};
let numberOfGetStatus = 0; let numberOfGetStatus = 0;
function getRndInteger(min: number, max: number) { function getRndInteger(min: number, max: number) {
@@ -75,276 +71,217 @@
numberOfGetStatus--; numberOfGetStatus--;
} }
} }
async function manuallyCleanupStorage() {
try {
loading.cleanup = true;
await post('/internal/cleanup', {});
return addToast({
message: 'Cleanup done.',
type: 'success'
});
} catch (error) {
return errorNotification(error);
} finally {
loading.cleanup = false;
}
}
</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">{$t('index.dashboard')}</div> <div class="mr-4 text-2xl tracking-tight">{$t('index.dashboard')}</div>
<button on:click={manuallyCleanupStorage} class:loading={loading.cleanup} class="btn btn-sm"
>Cleanup Storage</button
>
</div> </div>
<div class="mt-10 pb-12 tracking-tight sm:pb-16"> <div class="container lg:mx-auto lg:p-0 px-8 p-5">
<div class="mx-auto px-10"> {#if $appSession.teamId === '0'}
<div class="flex flex-col justify-center xl:flex-row"> <Usage />
{#if applications.length > 0} {/if}
<div> <h1 class="title lg:text-3xl mt-10">Applications</h1>
<div class="title">Resources</div> <div class="divider" />
<div class="flex items-start justify-center p-8"> <div class="grid grid-col gap-8 auto-cols-max grid-cols-1 lg:grid-cols-3 p-4">
<table class="rounded-none text-base"> {#if applications.length > 0}
<tbody> {#each applications as application}
{#each applications as application} <a class="no-underline mb-5" href={`/applications/${application.id}`}>
<tr> <div class="w-full rounded p-5 bg-coolgray-200 hover:bg-green-600 indicator">
<td class="space-x-2 items-center tracking-tight font-bold"> {#await getStatus(application)}
{#await getStatus(application)} <span class="indicator-item badge bg-yellow-500 badge-xs" />
<div class="inline-flex w-2 h-2 bg-yellow-500 rounded-full" /> {:then status}
{:then status} {#if status === 'Running'}
{#if status === 'Running'} <span class="indicator-item badge bg-success badge-xs" />
<div class="inline-flex w-2 h-2 bg-success rounded-full" /> {:else}
{:else} <span class="indicator-item badge bg-error badge-xs" />
<div class="inline-flex w-2 h-2 bg-error rounded-full" /> {/if}
{/if} {/await}
{/await} <div class="w-full flex flex-row">
<div class="inline-flex">{application.name}</div> <ApplicationsIcons {application} isAbsolute={true} />
</td> <div class="w-full flex flex-col">
<td class="px-10 inline-flex"> <h1 class="font-bold text-lg lg:text-sm truncate">
<ApplicationsIcons {application} isAbsolute={false} /> {application.name}
</td> {#if application.settings.isBot}
<td class="px-10"> <span class="text-xs">BOT</span>
<div {/if}
class="badge badge-outline text-xs border-applications rounded text-white" </h1>
<div class="h-10">
{#if application?.fqdn}
<h2>{application?.fqdn.replace('https://', '').replace('http://', '')}</h2>
{:else if !application.settings.isBot && !application?.fqdn}
<h2 class="text-red-500">Not configured</h2>
{/if}
</div>
<div class="flex justify-end items-end space-x-2 h-10">
{#if application.fqdn}
<a href={application.fqdn} target="_blank" class="icons hover:bg-green-500">
<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"
> >
Application <path stroke="none" d="M0 0h24v24H0z" fill="none" />
{#if application.settings.isBot} <path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
| BOT <line x1="10" y1="14" x2="20" y2="4" />
{/if} <polyline points="15 4 20 4 20 9" />
</div></td </svg>
</a>
{/if}
{#if application.settings.isBot && application.exposePort}
<a
href={`http://${dev ? 'localhost' : settings.ipv4}:${application.exposePort}`}
target="_blank"
class="icons hover:bg-green-500"
> >
<td class="flex justify-end"> <svg
{#if application.fqdn} xmlns="http://www.w3.org/2000/svg"
<a class="h-6 w-6"
href={application.fqdn} viewBox="0 0 24 24"
target="_blank" stroke-width="1.5"
class="icons bg-transparent text-sm inline-flex" stroke="currentColor"
><svg fill="none"
xmlns="http://www.w3.org/2000/svg" stroke-linecap="round"
class="h-6 w-6" stroke-linejoin="round"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg></a
>
{/if}
{#if application.settings.isBot && application.exposePort}
<a
href={`http://${dev ? 'localhost' : settings.ipv4}:${
application.exposePort
}`}
target="_blank"
class="icons bg-transparent text-sm inline-flex"
><svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg></a
>
{/if}
<a
href={`/applications/${application.id}`}
class="icons bg-transparent text-sm inline-flex"
> >
<svg <path stroke="none" d="M0 0h24v24H0z" fill="none" />
xmlns="http://www.w3.org/2000/svg" <path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
class="h-6 w-6" <line x1="10" y1="14" x2="20" y2="4" />
viewBox="0 0 24 24" <polyline points="15 4 20 4 20 9" />
stroke-width="1.5" </svg>
stroke="currentColor" </a>
fill="none" {/if}
stroke-linecap="round" </div>
stroke-linejoin="round" </div>
> </div>
<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>
</a>
</td>
</tr>
{/each}
{#each services as service}
<tr>
<td class="space-x-2 items-center tracking-tight font-bold">
{#await getStatus(service)}
<div class="inline-flex w-2 h-2 bg-yellow-500 rounded-full" />
{:then status}
{#if status === 'Running'}
<div class="inline-flex w-2 h-2 bg-success rounded-full" />
{:else}
<div class="inline-flex w-2 h-2 bg-error rounded-full" />
{/if}
{/await}
<div class="inline-flex">{service.name}</div>
</td>
<td class="px-10 inline-flex">
<ServiceIcons type={service.type} isAbsolute={false} />
</td>
<td class="px-10"
><div class="badge badge-outline text-xs border-services rounded text-white">
Service
</div>
</td>
<td class="flex justify-end">
{#if service.fqdn}
<a
href={service.fqdn}
target="_blank"
class="icons bg-transparent text-sm inline-flex"
><svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg></a
>
{/if}
<a
href={`/services/${service.id}`}
class="icons bg-transparent text-sm inline-flex"
>
<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>
</a>
</td>
</tr>
{/each}
{#each databases as database}
<tr>
<td class="space-x-2 items-center tracking-tight font-bold">
{#await getStatus(database)}
<div class="inline-flex w-2 h-2 bg-yellow-500 rounded-full" />
{:then status}
{#if status === 'Running'}
<div class="inline-flex w-2 h-2 bg-success rounded-full" />
{:else}
<div class="inline-flex w-2 h-2 bg-error rounded-full" />
{/if}
{/await}
<div class="inline-flex">{database.name}</div>
</td>
<td class="px-10 inline-flex">
<DatabaseIcons type={database.type} />
</td>
<td class="px-10">
<div class="badge badge-outline text-xs border-databases rounded text-white">
Database
</div>
</td>
<td class="flex justify-end">
<a
href={`/databases/${database.id}`}
class="icons bg-transparent text-sm inline-flex ml-11"
>
<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>
</a>
</td>
</tr>
{/each}
</tbody>
</table>
</div> </div>
</div> </a>
{/if} {/each}
{#if $appSession.teamId === '0'} {:else}
<Usage /> <h1 class="">Nothing is configured yet.</h1>
{/if} {/if}
</div> </div>
<h1 class="title lg:text-3xl mt-10">Services</h1>
<div class="divider" />
<div class="grid grid-col gap-8 auto-cols-max grid-cols-1 lg:grid-cols-3 p-4">
{#if services.length > 0}
{#each services as service}
<a class="no-underline mb-5" href={`/services/${service.id}`}>
<div class="w-full rounded p-5 bg-coolgray-200 hover:bg-pink-600 indicator">
{#await getStatus(service)}
<span class="indicator-item badge bg-yellow-500 badge-xs" />
{:then status}
{#if status === 'Running'}
<span class="indicator-item badge bg-success badge-xs" />
{:else}
<span class="indicator-item badge bg-error badge-xs" />
{/if}
{/await}
<div class="w-full flex flex-row">
<ServiceIcons type={service.type} isAbsolute={true} />
<div class="w-full flex flex-col">
<h1 class="font-bold text-lg lg:text-sm truncate">{service.name}</h1>
<div class="h-10">
{#if service?.fqdn}
<h2>{service?.fqdn.replace('https://', '').replace('http://', '')}</h2>
{:else}
<h2 class="text-red-500">Not configured</h2>
{/if}
</div>
<div class="flex justify-end items-end space-x-2 h-10">
{#if service.fqdn}
<a href={service.fqdn} target="_blank" class="icons hover:bg-pink-500">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg>
</a>
{/if}
</div>
</div>
</div>
</div>
</a>
{/each}
{:else}
<h1 class="">Nothing is configured yet.</h1>
{/if}
</div>
<h1 class="title lg:text-3xl mt-10">Databases</h1>
<div class="divider" />
<div class="grid grid-col gap-8 auto-cols-max grid-cols-1 lg:grid-cols-3 p-4 mb-32">
{#if databases.length > 0}
{#each databases as database}
<a class="no-underline mb-5" href={`/databases/${database.id}`}>
<div class="w-full rounded p-5 bg-coolgray-200 hover:bg-purple-500 indicator">
{#await getStatus(database)}
<span class="indicator-item badge bg-yellow-500 badge-xs" />
{:then status}
{#if status === 'Running'}
<span class="indicator-item badge bg-success badge-xs" />
{:else}
<span class="indicator-item badge bg-error badge-xs" />
{/if}
{/await}
<div class="w-full flex flex-row">
<DatabaseIcons type={database.type} isAbsolute={true} />
<div class="w-full flex flex-col">
<div class="h-10">
<h1 class="font-bold text-lg lg:text-sm truncate">{database.name}</h1>
<div class="h-10">
{#if database?.version}
<h2 class="text-xs">{database?.version}</h2>
{:else}
<h2 class="text-red-500">Not configured</h2>
{/if}
</div>
</div>
<div class="flex justify-end items-end space-x-2 h-10">
{#if database.settings.isPublic}
<div title="Public">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 "
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="12" cy="12" r="9" />
<line x1="3.6" y1="9" x2="20.4" y2="9" />
<line x1="3.6" y1="15" x2="20.4" y2="15" />
<path d="M11.5 3a17 17 0 0 0 0 18" />
<path d="M12.5 3a17 17 0 0 1 0 18" />
</svg>
</div>
{/if}
</div>
</div>
</div>
</div>
</a>
{/each}
{:else}
<h1 class="">Nothing is configured yet.</h1>
{/if}
</div> </div>
</div> </div>

View File

@@ -57,10 +57,18 @@
</a> </a>
{:else if service.type === 'appwrite'} {:else if service.type === 'appwrite'}
<a href="https://appwrite.io" target="_blank"> <a href="https://appwrite.io" target="_blank">
<Icons.Appwrite/> <Icons.Appwrite />
</a> </a>
{:else if service.type === 'moodle'} {:else if service.type === 'moodle'}
<a href="https://moodle.org" target="_blank"> <a href="https://moodle.org" target="_blank">
<Icons.Moodle /> <Icons.Moodle />
</a> </a>
{:else if service.type === 'glitchTip'}
<a href="https://glitchtip.com" target="_blank">
<Icons.GlitchTip />
</a>
{:else if service.type === 'searxng'}
<a href="https://searxng.org" target="_blank">
<Icons.Searxng />
</a>
{/if} {/if}

View File

@@ -1,12 +1,14 @@
<script lang="ts"> <script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
export let readOnly: any; export let readOnly: any;
export let service: any; export let service: any;
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5">
<div class="title">Ghost</div> <div class="title">Ghost</div>
<Explainer text={'You can change these values in the Ghost admin panel.'} />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="email">{$t('forms.default_email_address')}</label> <label for="email">{$t('forms.default_email_address')}</label>

View File

@@ -0,0 +1,208 @@
<script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import Setting from '$lib/components/Setting.svelte';
import { t } from '$lib/translations';
export let service: any;
function toggleEmailSmtpUseTls() {
service.glitchTip.emailSmtpUseTls = !service.glitchTip.emailSmtpUseTls;
}
function toggleEmailSmtpUseSsl() {
service.glitchTip.emailSmtpUseSsl = !service.glitchTip.emailSmtpUseSsl;
}
function toggleEnableOpenUserRegistration() {
service.glitchTip.enableOpenUserRegistration = !service.glitchTip.enableOpenUserRegistration;
}
</script>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">GlitchTip</div>
</div>
<div class="flex space-x-1 py-2 font-bold">
<div class="subtitle">Settings</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<Setting
bind:setting={service.glitchTip.enableOpenUserRegistration}
on:click={toggleEnableOpenUserRegistration}
title={'Enable Open User Registration'}
description={''}
/>
</div>
<div class="flex space-x-1 py-2 font-bold">
<div class="subtitle">Email settings</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="defaultEmailFrom" class="text-base font-bold text-stone-100">Default Email From</label
>
<CopyPasswordField
required
name="defaultEmailFrom"
id="defaultEmailFrom"
value={service.glitchTip.defaultEmailFrom}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailSmtpHost" class="text-base font-bold text-stone-100">SMTP Host</label>
<CopyPasswordField
name="emailSmtpHost"
id="emailSmtpHost"
value={service.glitchTip.emailSmtpHost}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailSmtpPort" class="text-base font-bold text-stone-100">SMTP Port</label>
<CopyPasswordField
name="emailSmtpPort"
id="emailSmtpPort"
value={service.glitchTip.emailSmtpPort}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailSmtpUser" class="text-base font-bold text-stone-100">SMTP User</label>
<CopyPasswordField
name="emailSmtpUser"
id="emailSmtpUser"
value={service.glitchTip.emailSmtpUser}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailSmtpPassword" class="text-base font-bold text-stone-100">SMTP Password</label>
<CopyPasswordField
name="emailSmtpPassword"
id="emailSmtpPassword"
value={service.glitchTip.emailSmtpPassword}
isPasswordField
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<Setting
bind:setting={service.glitchTip.emailSmtpUseTls}
on:click={toggleEmailSmtpUseTls}
title={'SMTP Use TLS'}
description={''}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<Setting
bind:setting={service.glitchTip.emailSmtpUseSsl}
on:click={toggleEmailSmtpUseSsl}
title={'SMTP Use SSL'}
description={''}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailBackend" class="text-base font-bold text-stone-100">Email Backend</label>
<CopyPasswordField name="emailBackend" id="emailBackend" value={service.glitchTip.emailBackend} />
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mailgunApiKey" class="text-base font-bold text-stone-100">Mailgun API Key</label>
<CopyPasswordField
name="mailgunApiKey"
id="mailgunApiKey"
value={service.glitchTip.mailgunApiKey}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="sendgridApiKey" class="text-base font-bold text-stone-100">SendGrid API Key</label>
<CopyPasswordField
name="sendgridApiKey"
id="sendgridApiKey"
value={service.glitchTip.sendgridApiKey}
/>
</div>
<div class="flex space-x-1 py-2 font-bold">
<div class="subtitle">Default User & Superuser</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="defaultEmail" class="text-base font-bold text-stone-100">{$t('forms.email')}</label>
<CopyPasswordField
name="defaultEmail"
id="defaultEmail"
value={service.glitchTip.defaultEmail}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="defaultUsername" class="text-base font-bold text-stone-100"
>{$t('forms.username')}</label
>
<CopyPasswordField
name="defaultUsername"
id="defaultUsername"
value={service.glitchTip.defaultUsername}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="defaultPassword" class="text-base font-bold text-stone-100"
>{$t('forms.password')}</label
>
<CopyPasswordField
name="defaultPassword"
id="defaultPassword"
value={service.glitchTip.defaultPassword}
readonly
disabled
isPasswordField
/>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">PostgreSQL</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlUser" class="text-base font-bold text-stone-100"
>{$t('forms.username')}</label
>
<CopyPasswordField
name="postgresqlUser"
id="postgresqlUser"
value={service.glitchTip.postgresqlUser}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlPassword" class="text-base font-bold text-stone-100"
>{$t('forms.password')}</label
>
<CopyPasswordField
id="postgresqlPassword"
isPasswordField
readonly
disabled
name="postgresqlPassword"
value={service.glitchTip.postgresqlPassword}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlDatabase" class="text-base font-bold text-stone-100"
>{$t('index.database')}</label
>
<CopyPasswordField
name="postgresqlDatabase"
id="postgresqlDatabase"
value={service.glitchTip.postgresqlDatabase}
readonly
disabled
/>
</div>

View File

@@ -0,0 +1,36 @@
<script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { t } from '$lib/translations';
export let service: any;
</script>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">SearXNG</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="secretKey">Secret Key</label>
<CopyPasswordField
name="secretKey"
id="secretKey"
isPasswordField
value={service.searxng.secretKey}
readonly
disabled
/>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">Redis</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="redisPassword">{$t('forms.password')}</label>
<CopyPasswordField
name="redisPassword"
id="redisPassword"
isPasswordField
value={service.searxng.redisPassword}
readonly
disabled
/>
</div>

View File

@@ -19,6 +19,7 @@
import Fider from './_Fider.svelte'; import Fider from './_Fider.svelte';
import Ghost from './_Ghost.svelte'; import Ghost from './_Ghost.svelte';
import GlitchTip from './_GlitchTip.svelte';
import Hasura from './_Hasura.svelte'; import Hasura from './_Hasura.svelte';
import MeiliSearch from './_MeiliSearch.svelte'; import MeiliSearch from './_MeiliSearch.svelte';
import MinIo from './_MinIO.svelte'; import MinIo from './_MinIO.svelte';
@@ -28,6 +29,7 @@
import Wordpress from './_Wordpress.svelte'; import Wordpress from './_Wordpress.svelte';
import Appwrite from './_Appwrite.svelte'; import Appwrite from './_Appwrite.svelte';
import Moodle from './_Moodle.svelte'; import Moodle from './_Moodle.svelte';
import Searxng from './_Searxng.svelte';
const { id } = $page.params; const { id } = $page.params;
$: isDisabled = $: isDisabled =
@@ -399,6 +401,10 @@
<Appwrite bind:service {readOnly} /> <Appwrite bind:service {readOnly} />
{:else if service.type === 'moodle'} {:else if service.type === 'moodle'}
<Moodle bind:service {readOnly} /> <Moodle bind:service {readOnly} />
{:else if service.type === 'glitchTip'}
<GlitchTip bind:service />
{:else if service.type === 'searxng'}
<Searxng bind:service />
{/if} {/if}
</div> </div>
</form> </form>

View File

@@ -62,7 +62,7 @@
</button> </button>
</div> </div>
<div class="flex-col justify-center"> <div class="flex-col justify-center mt-10 pb-12 sm:pb-16">
{#if !services || ownServices.length === 0} {#if !services || ownServices.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="text-center text-xl font-bold">{$t('service.no_service')}</div> <div class="text-center text-xl font-bold">{$t('service.no_service')}</div>

View File

@@ -56,7 +56,7 @@
</a> </a>
{/if} {/if}
</div> </div>
<div class="flex-col justify-center"> <div class="flex-col justify-center mt-10 pb-12 sm:pb-16">
{#if !sources || ownSources.length === 0} {#if !sources || ownSources.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="text-center text-xl font-bold">{$t('source.no_git_sources_found')}</div> <div class="text-center text-xl font-bold">{$t('source.no_git_sources_found')}</div>

View File

@@ -62,5 +62,5 @@ module.exports = {
scrollbar: ['dark'], scrollbar: ['dark'],
extend: {} extend: {}
}, },
plugins: [require('tailwindcss-scrollbar'), require('daisyui')] plugins: [require('tailwindcss-scrollbar'), require('daisyui'), require("@tailwindcss/typography")]
}; };

View File

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

553
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff