mirror of
https://github.com/ershisan99/coolify.git
synced 2025-12-18 12:33:06 +00:00
Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2315192f4b | ||
|
|
0faa1540f4 | ||
|
|
00cab67e73 | ||
|
|
b92bc9eebb | ||
|
|
1905db16e8 | ||
|
|
3e9cf7285b | ||
|
|
6fdbc572fe | ||
|
|
3fd50ebb12 | ||
|
|
0eb7f4526e | ||
|
|
646d92757a | ||
|
|
51efa01b11 | ||
|
|
dc4a63ef92 | ||
|
|
1b717ac091 | ||
|
|
e93d97f2bc | ||
|
|
45c904e876 | ||
|
|
880865f1f2 | ||
|
|
8e42203b89 | ||
|
|
2bd91fa970 | ||
|
|
a3fd95020d | ||
|
|
e5b1ce4eef | ||
|
|
531973baab | ||
|
|
b6e6a1ccf1 | ||
|
|
1140afe2c9 | ||
|
|
f8f17832de | ||
|
|
caaf030517 | ||
|
|
106aee31bd | ||
|
|
48fa4ff245 | ||
|
|
d75d2880e5 | ||
|
|
ec907b0ce4 | ||
|
|
2cda0b22c2 | ||
|
|
a0076db42e | ||
|
|
a37cf49c2a | ||
|
|
c4833c3cc2 | ||
|
|
d03fbd9224 | ||
|
|
5998212b82 | ||
|
|
62ccab22d6 | ||
|
|
5ccea1cfcc | ||
|
|
8ccb1bd34c | ||
|
|
c1a48dcf1e | ||
|
|
11d74c0c1f | ||
|
|
8290ee856f | ||
|
|
08332c8321 | ||
|
|
046f738b7d | ||
|
|
07708155ac | ||
|
|
df5e23c7c2 | ||
|
|
41adc02801 | ||
|
|
72b650b086 | ||
|
|
06fe3f33c0 | ||
|
|
cbabf7fc51 | ||
|
|
6aeafda604 |
@@ -2,5 +2,7 @@ COOLIFY_APP_ID=
|
||||
COOLIFY_SECRET_KEY=12341234123412341234123412341234
|
||||
COOLIFY_DATABASE_URL=file:../db/dev.db
|
||||
COOLIFY_SENTRY_DSN=
|
||||
COOLIFY_IS_ON="docker"
|
||||
COOLIFY_WHITE_LABELED="false"
|
||||
COOLIFY_IS_ON=docker
|
||||
COOLIFY_WHITE_LABELED=false
|
||||
COOLIFY_WHITE_LABELED_ICON=
|
||||
COOLIFY_AUTO_UPDATE=false
|
||||
183
CONTRIBUTING.md
183
CONTRIBUTING.md
@@ -12,9 +12,6 @@ This is a little list of what you can do to help the project:
|
||||
|
||||
- [🧑💻 Develop your own ideas](#developer-contribution)
|
||||
- [🌐 Translate the project](#translation)
|
||||
- [📄 Help sorting out the issues](#help-sorting-out-the-issues)
|
||||
- [🎯 Test Pull Requests](#test-pull-requests)
|
||||
- [✒️ Help with the documentation](#help-with-the-documentation)
|
||||
|
||||
## 👋 Introduction
|
||||
|
||||
@@ -60,6 +57,7 @@ You need to have [Docker Engine](https://docs.docker.com/engine/install/) instal
|
||||
- **Languages**: Node.js / Javascript / Typescript
|
||||
- **Framework JS/TS**: Svelte / SvelteKit
|
||||
- **Database ORM**: Prisma.io
|
||||
- **Docker Engine**
|
||||
|
||||
### Database migrations
|
||||
|
||||
@@ -83,50 +81,159 @@ You can add any open-source and self-hostable software (service/application) to
|
||||
|
||||
## Backend
|
||||
|
||||
I use MinIO as an example.
|
||||
There are 5 steps you should make on the backend side.
|
||||
|
||||
You need to add a new folder to [src/routes/services/[id]](src/routes/services/[id]) with the low-capital name of the service. It should have three files with the following properties:
|
||||
1. Create Prisma / database schema for the new service.
|
||||
2. Add supported versions of the service.
|
||||
3. Update global functions.
|
||||
4. Create API endpoints.
|
||||
5. Define automatically generated variables.
|
||||
|
||||
1. `index.json.ts`: A POST endpoint that updates Coolify's database about the service.
|
||||
> I will use [Umami](https://umami.is/) as an example service.
|
||||
|
||||
Basic services only require updating the URL(fqdn) and the name of the service.
|
||||
### Create Prisma / database schema for the new service.
|
||||
|
||||
2. `start.json.ts`: A start endpoint that setups the docker-compose file (for Local Docker Engines) and starts the service.
|
||||
You only need to do this if you store passwords or any persistent configuration. Mostly it is required by all services, but there are some exceptions, like NocoDB.
|
||||
|
||||
- To start a service, you need to know Coolify supported images and tags of the service. For that you need to update `supportedServiceTypesAndVersions` function at [src/lib/components/common.ts](src/lib/components/common.ts).
|
||||
Update Prisma schema in [prisma/schema.prisma](prisma/schema.prisma).
|
||||
|
||||
Example JSON:
|
||||
- Add new model with the new service name.
|
||||
- Make a relationshup with `Service` model.
|
||||
- In the `Service` model, the name of the new field should be with low-capital.
|
||||
- If the service needs a database, define a `publicPort` field to be able to make it's database public, example field name in case of PostgreSQL: `postgresqlPublicPort`. It should be a optional field.
|
||||
|
||||
```js
|
||||
If you are finished with the Prisma schema, you should update the database schema with `pnpm db:push` command.
|
||||
|
||||
> You must restart the running development environment to be able to use the new model
|
||||
|
||||
> If you use VSCode, you probably need to restart the `Typescript Language Server` to get the new types loaded in the running VSCode.
|
||||
|
||||
### Add supported versions
|
||||
|
||||
Supported versions are hardcoded into Coolify (for now).
|
||||
|
||||
You need to update `supportedServiceTypesAndVersions` function at [src/lib/components/common.ts](src/lib/components/common.ts). Example JSON:
|
||||
|
||||
```js
|
||||
{
|
||||
// Name used to identify the service in Coolify
|
||||
name: 'minio',
|
||||
// Name used to identify the service internally
|
||||
name: 'umami',
|
||||
// Fancier name to show to the user
|
||||
fancyName: 'MinIO',
|
||||
fancyName: 'Umami',
|
||||
// Docker base image for the service
|
||||
baseImage: 'minio/minio',
|
||||
baseImage: 'ghcr.io/mikecao/umami',
|
||||
// Optional: If there is any dependent image, you should list it here
|
||||
images: [],
|
||||
// Usable tags
|
||||
versions: ['latest'],
|
||||
versions: ['postgresql-latest'],
|
||||
// Which tag is the recommended
|
||||
recommendedVersion: 'latest',
|
||||
// Application's default port, MinIO listens on 9001 (and 9000, more details later on)
|
||||
recommendedVersion: 'postgresql-latest',
|
||||
// Application's default port, Umami listens on 3000
|
||||
ports: {
|
||||
main: 9001
|
||||
main: 3000
|
||||
}
|
||||
},
|
||||
```
|
||||
}
|
||||
```
|
||||
|
||||
- You need to define a compose file as `const composeFile: ComposeFile` found in [src/routes/services/[id]/minio/start.json.ts](src/routes/services/[id]/minio/start.json.ts)
|
||||
### Update global functions
|
||||
|
||||
**IMPORTANT:** It should contain `all the default environment variables` that are required for the service to function correctly and `all the volumes to persist data` in restarts.
|
||||
1. Add the new service to the `include` variable in [src/lib/database/services.ts](src/lib/database/services.ts), so it will be included in all places in the database queries where it is required.
|
||||
|
||||
- You could also define an `HTTP` or `TCP` proxy for every other port that should be proxied to your server. (See `startHttpProxy` and `startTcpProxy` functions in [src/lib/haproxy/index.ts](src/lib/haproxy/index.ts))
|
||||
```js
|
||||
const include: Prisma.ServiceInclude = {
|
||||
destinationDocker: true,
|
||||
persistentStorage: true,
|
||||
serviceSecret: true,
|
||||
minio: true,
|
||||
plausibleAnalytics: true,
|
||||
vscodeserver: true,
|
||||
wordpress: true,
|
||||
ghost: true,
|
||||
meiliSearch: true,
|
||||
umami: true // This line!
|
||||
};
|
||||
```
|
||||
|
||||
3. `stop.json.ts` A stop endpoint that stops the service.
|
||||
2. Update the database update query with the new service type to `configureServiceType` function in [src/lib/database/services.ts](src/lib/database/services.ts). This function defines the automatically generated variables (passwords, users, etc.) and it's encryption process (if applicable).
|
||||
|
||||
It needs to stop all the services by their container name and proxies (if applicable).
|
||||
```js
|
||||
[...]
|
||||
else if (type === 'umami') {
|
||||
const postgresqlUser = cuid();
|
||||
const postgresqlPassword = encrypt(generatePassword());
|
||||
const postgresqlDatabase = 'umami';
|
||||
const hashSalt = encrypt(generatePassword(64));
|
||||
await prisma.service.update({
|
||||
where: { id },
|
||||
data: {
|
||||
type,
|
||||
umami: {
|
||||
create: {
|
||||
postgresqlDatabase,
|
||||
postgresqlPassword,
|
||||
postgresqlUser,
|
||||
hashSalt,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
4. You need to add the automatically generated variables (passwords, users, etc.) for the new service at [src/lib/database/services.ts](src/lib/database/services.ts), `configureServiceType` function.
|
||||
3. Add decryption process for configurations and passwords to `getService` function in [src/lib/database/services.ts](src/lib/database/services.ts)
|
||||
|
||||
```js
|
||||
if (body.umami?.postgresqlPassword)
|
||||
body.umami.postgresqlPassword = decrypt(body.umami.postgresqlPassword);
|
||||
|
||||
if (body.umami?.hashSalt) body.umami.hashSalt = decrypt(body.umami.hashSalt);
|
||||
```
|
||||
|
||||
4. Add service deletion query to `removeService` function in [src/lib/database/services.ts](src/lib/database/services.ts)
|
||||
|
||||
### Create API endpoints.
|
||||
|
||||
You need to add a new folder under [src/routes/services/[id]](src/routes/services/[id]) with the low-capital name of the service. You need 3 default files in that folder.
|
||||
|
||||
#### `index.json.ts`:
|
||||
|
||||
It has a POST endpoint that updates the service details in Coolify's database, such as name, url, other configurations, like passwords. It should look something like this:
|
||||
|
||||
```js
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { ErrorHandler } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
|
||||
let { name, fqdn } = await event.request.json();
|
||||
if (fqdn) fqdn = fqdn.toLowerCase();
|
||||
|
||||
try {
|
||||
await db.updateService({ id, fqdn, name });
|
||||
return { status: 201 };
|
||||
} catch (error) {
|
||||
return ErrorHandler(error);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
If it's necessary, you can create your own database update function, specifically for the new service.
|
||||
|
||||
#### `start.json.ts`
|
||||
|
||||
It has a POST endpoint that sets all the required secrets, persistent volumes, `docker-compose.yaml` file and sends a request to the specified docker engine.
|
||||
|
||||
You could also define an `HTTP` or `TCP` proxy for every other port that should be proxied to your server. (See `startHttpProxy` and `startTcpProxy` functions in [src/lib/haproxy/index.ts](src/lib/haproxy/index.ts))
|
||||
|
||||
#### `stop.json.ts`
|
||||
|
||||
It has a POST endpoint that stops the service and all dependent (TCP/HTTP proxies) containers. If publicPort is specified it also needs to cleanup it from the database.
|
||||
|
||||
## Frontend
|
||||
|
||||
@@ -134,7 +241,11 @@ You need to add a new folder to [src/routes/services/[id]](src/routes/services/[
|
||||
|
||||
SVG is recommended, but you can use PNG as well. It should have the `isAbsolute` variable with the suitable CSS classes, primarily for sizing and positioning.
|
||||
|
||||
2. You need to include it the logo at [src/routes/services/index.svelte](src/routes/services/index.svelte) with `isAbsolute` and [src/lib/components/ServiceLinks.svelte](src/lib/components/ServiceLinks.svelte) with a link to the docs/main site of the service.
|
||||
2. You need to include it the logo at
|
||||
|
||||
- [src/routes/services/index.svelte](src/routes/services/index.svelte) with `isAbsolute` in two places,
|
||||
- [src/lib/components/ServiceLinks.svelte](src/lib/components/ServiceLinks.svelte) with `isAbsolute` and a link to the docs/main site of the service
|
||||
- [src/routes/services/[id]/configuration/type.svelte](src/routes/services/[id]/configuration/type.svelte) with `isAbsolute`.
|
||||
|
||||
3. By default the URL and the name frontend forms are included in [src/routes/services/[id]/\_Services/\_Services.svelte](src/routes/services/[id]/_Services/_Services.svelte).
|
||||
|
||||
@@ -164,19 +275,3 @@ If your language doesn't appear in the [locales folder list](src/lib/locales/),
|
||||
1. In `src/lib/locales/`, Copy paste `en.json` and rename it with your language (eg: `cz.json`).
|
||||
2. In the [lang.json](src/lib/lang.json) file, add a line after the first bracket (`{`) with `"ISO of your language": "Language",` (eg: `"cz": "Czech",`).
|
||||
3. Have fun translating!
|
||||
|
||||
### Additionnal pull requests steps
|
||||
|
||||
Please add the emoji 🌐 to your pull request title to indicate that it is a translation.
|
||||
|
||||
## 📄 Help sorting out the issues
|
||||
|
||||
ToDo
|
||||
|
||||
## 🎯 Test Pull Requests
|
||||
|
||||
ToDo
|
||||
|
||||
## ✒️ Help with the documentation
|
||||
|
||||
ToDo
|
||||
|
||||
12
README.md
12
README.md
@@ -13,10 +13,16 @@ https://demo.coolify.io/
|
||||
Installation is automated with the following command:
|
||||
|
||||
```bash
|
||||
/bin/bash -c "$(curl -fsSL https://get.coollabs.io/coolify/install.sh)"
|
||||
wget -q https://get.coollabs.io/coolify/install.sh -O install.sh; sudo bash ./install.sh
|
||||
```
|
||||
|
||||
If you would like no questions during installation
|
||||
If you would like no questions during installation:
|
||||
|
||||
```bash
|
||||
wget -q https://get.coollabs.io/coolify/install.sh -O install.sh; sudo bash ./install.sh -f
|
||||
```
|
||||
|
||||
For more details goto the [docs](https://docs.coollabs.io/coolify/installation).
|
||||
|
||||
## Features
|
||||
|
||||
@@ -77,6 +83,8 @@ You can host cool open-source services as well:
|
||||
- [LanguageTool](https://languagetool.org)
|
||||
- [n8n](https://n8n.io)
|
||||
- [Uptime Kuma](https://github.com/louislam/uptime-kuma)
|
||||
- [MeiliSearch](https://github.com/meilisearch/meilisearch)
|
||||
- [Umami](https://github.com/mikecao/umami)
|
||||
|
||||
## Migration from v1
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "coolify",
|
||||
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
|
||||
"version": "2.5.1",
|
||||
"version": "2.6.0",
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
"dev": "docker-compose -f docker-compose-dev.yaml up -d && cross-env NODE_ENV=development & svelte-kit dev --host 0.0.0.0",
|
||||
@@ -55,8 +55,8 @@
|
||||
"svelte-check": "2.7.0",
|
||||
"svelte-preprocess": "4.10.6",
|
||||
"svelte-select": "4.4.7",
|
||||
"tailwindcss": "3.0.24",
|
||||
"sveltekit-i18n": "2.1.2",
|
||||
"tailwindcss": "3.0.24",
|
||||
"ts-node": "10.7.0",
|
||||
"tslib": "2.3.1",
|
||||
"typescript": "4.6.3"
|
||||
|
||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@@ -48,8 +48,8 @@ specifiers:
|
||||
svelte-kit-cookie-session: 2.1.3
|
||||
svelte-preprocess: 4.10.6
|
||||
svelte-select: 4.4.7
|
||||
tailwindcss: 3.0.24
|
||||
sveltekit-i18n: 2.1.2
|
||||
tailwindcss: 3.0.24
|
||||
tailwindcss-scrollbar: 0.1.0
|
||||
ts-node: 10.7.0
|
||||
tslib: 2.3.1
|
||||
@@ -108,9 +108,9 @@ devDependencies:
|
||||
svelte-check: 2.7.0_postcss@8.4.12+svelte@3.47.0
|
||||
svelte-preprocess: 4.10.6_41810887ae6c6d59323116f47e33fa38
|
||||
svelte-select: 4.4.7
|
||||
sveltekit-i18n: 2.1.2_svelte@3.47.0
|
||||
tailwindcss: 3.0.24_ts-node@10.7.0
|
||||
ts-node: 10.7.0_de7c86b0cde507c63a0402da5b982bd3
|
||||
sveltekit-i18n: 2.1.2_svelte@3.46.4
|
||||
tslib: 2.3.1
|
||||
typescript: 4.6.3
|
||||
|
||||
@@ -424,7 +424,7 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@sveltekit-i18n/base/1.1.1_svelte@3.46.4:
|
||||
/@sveltekit-i18n/base/1.1.1_svelte@3.47.0:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-J/sMU0OwS3dCLOuilHMBqu8vZHuuXiNV9vFJx8Nb4/b5BlR/KCZ4bCXI8wZR02GHeCOYKZxWus07CM1scxa/jw==
|
||||
@@ -432,7 +432,7 @@ packages:
|
||||
peerDependencies:
|
||||
svelte: ^3.x
|
||||
dependencies:
|
||||
svelte: 3.46.4
|
||||
svelte: 3.47.0
|
||||
optionalDependencies:
|
||||
'@sveltekit-i18n/parser-default': 1.0.3
|
||||
dev: true
|
||||
@@ -4977,7 +4977,7 @@ packages:
|
||||
engines: { node: '>= 8' }
|
||||
dev: true
|
||||
|
||||
/sveltekit-i18n/2.1.2_svelte@3.46.4:
|
||||
/sveltekit-i18n/2.1.2_svelte@3.47.0:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-s5YxcbNd2EWNZaZR1A4Drt8s53E4fpUkN4XIWd3VRpw1pihZVWssqmBW1qkjQ6AB0kiu1Qwule+vt1HkbQOjrg==
|
||||
@@ -4985,9 +4985,9 @@ packages:
|
||||
peerDependencies:
|
||||
svelte: ^3.x
|
||||
dependencies:
|
||||
'@sveltekit-i18n/base': 1.1.1_svelte@3.46.4
|
||||
'@sveltekit-i18n/base': 1.1.1_svelte@3.47.0
|
||||
'@sveltekit-i18n/parser-default': 1.0.3
|
||||
svelte: 3.46.4
|
||||
svelte: 3.47.0
|
||||
dev: true
|
||||
|
||||
/table/6.7.2:
|
||||
|
||||
17
prisma/migrations/20220425071132_umami/migration.sql
Normal file
17
prisma/migrations/20220425071132_umami/migration.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Umami" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"serviceId" TEXT NOT NULL,
|
||||
"postgresqlUser" TEXT NOT NULL,
|
||||
"postgresqlPassword" TEXT NOT NULL,
|
||||
"postgresqlDatabase" TEXT NOT NULL,
|
||||
"postgresqlPublicPort" INTEGER,
|
||||
"umamiAdminPassword" TEXT NOT NULL,
|
||||
"hashSalt" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "Umami_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Umami_serviceId_key" ON "Umami"("serviceId");
|
||||
@@ -0,0 +1,22 @@
|
||||
-- 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,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
INSERT INTO "new_Setting" ("createdAt", "dualCerts", "fqdn", "id", "isRegistrationEnabled", "maxPort", "minPort", "proxyHash", "proxyPassword", "proxyUser", "updatedAt") SELECT "createdAt", "dualCerts", "fqdn", "id", "isRegistrationEnabled", "maxPort", "minPort", "proxyHash", "proxyPassword", "proxyUser", "updatedAt" FROM "Setting";
|
||||
DROP TABLE "Setting";
|
||||
ALTER TABLE "new_Setting" RENAME TO "Setting";
|
||||
CREATE UNIQUE INDEX "Setting_fqdn_key" ON "Setting"("fqdn");
|
||||
PRAGMA foreign_key_check;
|
||||
PRAGMA foreign_keys=ON;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Application" ADD COLUMN "baseBuildImage" TEXT;
|
||||
ALTER TABLE "Application" ADD COLUMN "baseImage" TEXT;
|
||||
16
prisma/migrations/20220427133656_hasura/migration.sql
Normal file
16
prisma/migrations/20220427133656_hasura/migration.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Hasura" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"serviceId" TEXT NOT NULL,
|
||||
"postgresqlUser" TEXT NOT NULL,
|
||||
"postgresqlPassword" TEXT NOT NULL,
|
||||
"postgresqlDatabase" TEXT NOT NULL,
|
||||
"postgresqlPublicPort" INTEGER,
|
||||
"graphQLAdminPassword" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "Hasura_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Hasura_serviceId_key" ON "Hasura"("serviceId");
|
||||
25
prisma/migrations/20220429202516_fider/migration.sql
Normal file
25
prisma/migrations/20220429202516_fider/migration.sql
Normal file
@@ -0,0 +1,25 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Fider" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"serviceId" TEXT NOT NULL,
|
||||
"postgresqlUser" TEXT NOT NULL,
|
||||
"postgresqlPassword" TEXT NOT NULL,
|
||||
"postgresqlDatabase" TEXT NOT NULL,
|
||||
"postgresqlPublicPort" INTEGER,
|
||||
"jwtSecret" TEXT NOT NULL,
|
||||
"emailNoreply" TEXT,
|
||||
"emailMailgunApiKey" TEXT,
|
||||
"emailMailgunDomain" TEXT,
|
||||
"emailMailgunRegion" TEXT,
|
||||
"emailSmtpHost" TEXT,
|
||||
"emailSmtpPort" INTEGER,
|
||||
"emailSmtpUser" TEXT,
|
||||
"emailSmtpPassword" TEXT,
|
||||
"emailSmtpEnableStartTls" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "Fider_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Fider_serviceId_key" ON "Fider"("serviceId");
|
||||
@@ -0,0 +1,29 @@
|
||||
-- RedefineTables
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_Fider" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"serviceId" TEXT NOT NULL,
|
||||
"postgresqlUser" TEXT NOT NULL,
|
||||
"postgresqlPassword" TEXT NOT NULL,
|
||||
"postgresqlDatabase" TEXT NOT NULL,
|
||||
"postgresqlPublicPort" INTEGER,
|
||||
"jwtSecret" TEXT NOT NULL,
|
||||
"emailNoreply" TEXT,
|
||||
"emailMailgunApiKey" TEXT,
|
||||
"emailMailgunDomain" TEXT,
|
||||
"emailMailgunRegion" TEXT NOT NULL DEFAULT 'EU',
|
||||
"emailSmtpHost" TEXT,
|
||||
"emailSmtpPort" INTEGER,
|
||||
"emailSmtpUser" TEXT,
|
||||
"emailSmtpPassword" TEXT,
|
||||
"emailSmtpEnableStartTls" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "Fider_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_Fider" ("createdAt", "emailMailgunApiKey", "emailMailgunDomain", "emailMailgunRegion", "emailNoreply", "emailSmtpEnableStartTls", "emailSmtpHost", "emailSmtpPassword", "emailSmtpPort", "emailSmtpUser", "id", "jwtSecret", "postgresqlDatabase", "postgresqlPassword", "postgresqlPublicPort", "postgresqlUser", "serviceId", "updatedAt") SELECT "createdAt", "emailMailgunApiKey", "emailMailgunDomain", coalesce("emailMailgunRegion", 'EU') AS "emailMailgunRegion", "emailNoreply", "emailSmtpEnableStartTls", "emailSmtpHost", "emailSmtpPassword", "emailSmtpPort", "emailSmtpUser", "id", "jwtSecret", "postgresqlDatabase", "postgresqlPassword", "postgresqlPublicPort", "postgresqlUser", "serviceId", "updatedAt" FROM "Fider";
|
||||
DROP TABLE "Fider";
|
||||
ALTER TABLE "new_Fider" RENAME TO "Fider";
|
||||
CREATE UNIQUE INDEX "Fider_serviceId_key" ON "Fider"("serviceId");
|
||||
PRAGMA foreign_key_check;
|
||||
PRAGMA foreign_keys=ON;
|
||||
@@ -0,0 +1,23 @@
|
||||
-- RedefineTables
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_Setting" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"fqdn" TEXT,
|
||||
"isRegistrationEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
|
||||
"minPort" INTEGER NOT NULL DEFAULT 9000,
|
||||
"maxPort" INTEGER NOT NULL DEFAULT 9100,
|
||||
"proxyPassword" TEXT NOT NULL,
|
||||
"proxyUser" TEXT NOT NULL,
|
||||
"proxyHash" TEXT,
|
||||
"isAutoUpdateEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"isDNSCheckEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
INSERT INTO "new_Setting" ("createdAt", "dualCerts", "fqdn", "id", "isAutoUpdateEnabled", "isRegistrationEnabled", "maxPort", "minPort", "proxyHash", "proxyPassword", "proxyUser", "updatedAt") SELECT "createdAt", "dualCerts", "fqdn", "id", "isAutoUpdateEnabled", "isRegistrationEnabled", "maxPort", "minPort", "proxyHash", "proxyPassword", "proxyUser", "updatedAt" FROM "Setting";
|
||||
DROP TABLE "Setting";
|
||||
ALTER TABLE "new_Setting" RENAME TO "Setting";
|
||||
CREATE UNIQUE INDEX "Setting_fqdn_key" ON "Setting"("fqdn");
|
||||
PRAGMA foreign_key_check;
|
||||
PRAGMA foreign_keys=ON;
|
||||
@@ -18,6 +18,8 @@ model Setting {
|
||||
proxyPassword String
|
||||
proxyUser String
|
||||
proxyHash String?
|
||||
isAutoUpdateEnabled Boolean @default(false)
|
||||
isDNSCheckEnabled Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
@@ -91,9 +93,9 @@ model Application {
|
||||
pythonWSGI String?
|
||||
pythonModule String?
|
||||
pythonVariable String?
|
||||
dockerFileLocation String?
|
||||
dockerFileLocation String?
|
||||
denoMainFile String?
|
||||
denoOptions String?
|
||||
denoOptions String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
settings ApplicationSettings?
|
||||
@@ -104,6 +106,8 @@ model Application {
|
||||
gitSource GitSource? @relation(fields: [gitSourceId], references: [id])
|
||||
secrets Secret[]
|
||||
persistentStorage ApplicationPersistentStorage[]
|
||||
baseImage String?
|
||||
baseBuildImage String?
|
||||
}
|
||||
|
||||
model ApplicationSettings {
|
||||
@@ -301,6 +305,9 @@ model Service {
|
||||
serviceSecret ServiceSecret[]
|
||||
meiliSearch MeiliSearch?
|
||||
persistentStorage ServicePersistentStorage[]
|
||||
umami Umami?
|
||||
hasura Hasura?
|
||||
fider Fider?
|
||||
}
|
||||
|
||||
model PlausibleAnalytics {
|
||||
@@ -385,3 +392,52 @@ model MeiliSearch {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Umami {
|
||||
id String @id @default(cuid())
|
||||
serviceId String @unique
|
||||
postgresqlUser String
|
||||
postgresqlPassword String
|
||||
postgresqlDatabase String
|
||||
postgresqlPublicPort Int?
|
||||
umamiAdminPassword String
|
||||
hashSalt String
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Hasura {
|
||||
id String @id @default(cuid())
|
||||
serviceId String @unique
|
||||
postgresqlUser String
|
||||
postgresqlPassword String
|
||||
postgresqlDatabase String
|
||||
postgresqlPublicPort Int?
|
||||
graphQLAdminPassword String
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Fider {
|
||||
id String @id @default(cuid())
|
||||
serviceId String @unique
|
||||
postgresqlUser String
|
||||
postgresqlPassword String
|
||||
postgresqlDatabase String
|
||||
postgresqlPublicPort Int?
|
||||
jwtSecret String
|
||||
emailNoreply String?
|
||||
emailMailgunApiKey String?
|
||||
emailMailgunDomain String?
|
||||
emailMailgunRegion String @default("EU")
|
||||
emailSmtpHost String?
|
||||
emailSmtpPort Int?
|
||||
emailSmtpUser String?
|
||||
emailSmtpPassword String?
|
||||
emailSmtpEnableStartTls Boolean @default(false)
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
@@ -50,6 +50,20 @@ async function main() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Set auto-update based on env variable
|
||||
const isAutoUpdateEnabled = process.env['COOLIFY_AUTO_UPDATE'] === 'true';
|
||||
const settings = await prisma.setting.findFirst({});
|
||||
if (settings) {
|
||||
await prisma.setting.update({
|
||||
where: {
|
||||
id: settings.id
|
||||
},
|
||||
data: {
|
||||
isAutoUpdateEnabled
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
main()
|
||||
.catch((e) => {
|
||||
|
||||
@@ -5,6 +5,19 @@ import { scanningTemplates } from '$lib/components/templates';
|
||||
import { promises as fs } from 'fs';
|
||||
import { staticDeployments } from '$lib/components/common';
|
||||
|
||||
const staticApps = ['static', 'react', 'vuejs', 'svelte', 'gatsby', 'astro', 'eleventy'];
|
||||
const nodeBased = [
|
||||
'react',
|
||||
'vuejs',
|
||||
'svelte',
|
||||
'gatsby',
|
||||
'php',
|
||||
'astro',
|
||||
'eleventy',
|
||||
'node',
|
||||
'nestjs'
|
||||
];
|
||||
|
||||
export function makeLabelForStandaloneApplication({
|
||||
applicationId,
|
||||
fqdn,
|
||||
@@ -104,11 +117,12 @@ export const setDefaultConfiguration = async (data) => {
|
||||
else if (buildPack === 'php') port = 80;
|
||||
else if (buildPack === 'python') port = 8000;
|
||||
}
|
||||
if (!installCommand && buildPack !== 'static')
|
||||
if (!installCommand && buildPack !== 'static' && buildPack !== 'laravel')
|
||||
installCommand = template?.installCommand || 'yarn install';
|
||||
if (!startCommand && buildPack !== 'static')
|
||||
if (!startCommand && buildPack !== 'static' && buildPack !== 'laravel')
|
||||
startCommand = template?.startCommand || 'yarn start';
|
||||
if (!buildCommand && buildPack !== 'static') buildCommand = template?.buildCommand || null;
|
||||
if (!buildCommand && buildPack !== 'static' && buildPack !== 'laravel')
|
||||
buildCommand = template?.buildCommand || null;
|
||||
if (!publishDirectory) publishDirectory = template?.publishDirectory || null;
|
||||
if (baseDirectory) {
|
||||
if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`;
|
||||
@@ -137,7 +151,13 @@ export const setDefaultConfiguration = async (data) => {
|
||||
};
|
||||
};
|
||||
|
||||
export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId) {
|
||||
export async function copyBaseConfigurationFiles(
|
||||
buildPack,
|
||||
workdir,
|
||||
buildId,
|
||||
applicationId,
|
||||
baseImage
|
||||
) {
|
||||
try {
|
||||
if (buildPack === 'php') {
|
||||
await fs.writeFile(`${workdir}/entrypoint.sh`, `chown -R 1000 /app`);
|
||||
@@ -146,7 +166,7 @@ export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, ap
|
||||
buildId,
|
||||
applicationId
|
||||
});
|
||||
} else if (staticDeployments.includes(buildPack)) {
|
||||
} else if (staticDeployments.includes(buildPack) && baseImage.includes('nginx')) {
|
||||
await fs.writeFile(
|
||||
`${workdir}/nginx.conf`,
|
||||
`user nginx;
|
||||
@@ -174,7 +194,7 @@ export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, ap
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
@@ -199,11 +219,6 @@ export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, ap
|
||||
}
|
||||
`
|
||||
);
|
||||
await saveBuildLog({
|
||||
line: 'Copied default configuration file for Nginx.',
|
||||
buildId,
|
||||
applicationId
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
@@ -218,3 +233,215 @@ export function checkPnpm(installCommand = null, buildCommand = null, startComma
|
||||
startCommand?.includes('pnpm')
|
||||
);
|
||||
}
|
||||
|
||||
export function setDefaultBaseImage(buildPack) {
|
||||
const nodeVersions = [
|
||||
{
|
||||
value: 'node:lts',
|
||||
label: 'node:lts'
|
||||
},
|
||||
{
|
||||
value: 'node:18',
|
||||
label: 'node:18'
|
||||
},
|
||||
{
|
||||
value: 'node:17',
|
||||
label: 'node:17'
|
||||
},
|
||||
{
|
||||
value: 'node:16',
|
||||
label: 'node:16'
|
||||
},
|
||||
{
|
||||
value: 'node:14',
|
||||
label: 'node:14'
|
||||
},
|
||||
{
|
||||
value: 'node:12',
|
||||
label: 'node:12'
|
||||
}
|
||||
];
|
||||
const staticVersions = [
|
||||
{
|
||||
value: 'webdevops/nginx:alpine',
|
||||
label: 'webdevops/nginx:alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/apache:alpine',
|
||||
label: 'webdevops/apache:alpine'
|
||||
}
|
||||
];
|
||||
const rustVersions = [
|
||||
{
|
||||
value: 'rust:latest',
|
||||
label: 'rust:latest'
|
||||
},
|
||||
{
|
||||
value: 'rust:1.60',
|
||||
label: 'rust:1.60'
|
||||
},
|
||||
{
|
||||
value: 'rust:1.60-buster',
|
||||
label: 'rust:1.60-buster'
|
||||
},
|
||||
{
|
||||
value: 'rust:1.60-bullseye',
|
||||
label: 'rust:1.60-bullseye'
|
||||
},
|
||||
{
|
||||
value: 'rust:1.60-slim-buster',
|
||||
label: 'rust:1.60-slim-buster'
|
||||
},
|
||||
{
|
||||
value: 'rust:1.60-slim-bullseye',
|
||||
label: 'rust:1.60-slim-bullseye'
|
||||
},
|
||||
{
|
||||
value: 'rust:1.60-alpine3.14',
|
||||
label: 'rust:1.60-alpine3.14'
|
||||
},
|
||||
{
|
||||
value: 'rust:1.60-alpine3.15',
|
||||
label: 'rust:1.60-alpine3.15'
|
||||
}
|
||||
];
|
||||
const phpVersions = [
|
||||
{
|
||||
value: 'webdevops/php-apache:8.0',
|
||||
label: 'webdevops/php-apache:8.0'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:8.0',
|
||||
label: 'webdevops/php-nginx:8.0'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:7.4',
|
||||
label: 'webdevops/php-apache:7.4'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:7.4',
|
||||
label: 'webdevops/php-nginx:7.4'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:7.3',
|
||||
label: 'webdevops/php-apache:7.3'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:7.3',
|
||||
label: 'webdevops/php-nginx:7.3'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:7.2',
|
||||
label: 'webdevops/php-apache:7.2'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:7.2',
|
||||
label: 'webdevops/php-nginx:7.2'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:7.1',
|
||||
label: 'webdevops/php-apache:7.1'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:7.1',
|
||||
label: 'webdevops/php-nginx:7.1'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:7.0',
|
||||
label: 'webdevops/php-apache:7.0'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:7.0',
|
||||
label: 'webdevops/php-nginx:7.0'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:5.6',
|
||||
label: 'webdevops/php-apache:5.6'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:5.6',
|
||||
label: 'webdevops/php-nginx:5.6'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:8.0-alpine',
|
||||
label: 'webdevops/php-apache:8.0-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:8.0-alpine',
|
||||
label: 'webdevops/php-nginx:8.0-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:7.4-alpine',
|
||||
label: 'webdevops/php-apache:7.4-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:7.4-alpine',
|
||||
label: 'webdevops/php-nginx:7.4-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:7.3-alpine',
|
||||
label: 'webdevops/php-apache:7.3-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:7.3-alpine',
|
||||
label: 'webdevops/php-nginx:7.3-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:7.2-alpine',
|
||||
label: 'webdevops/php-apache:7.2-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:7.2-alpine',
|
||||
label: 'webdevops/php-nginx:7.2-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:7.1-alpine',
|
||||
label: 'webdevops/php-apache:7.1-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:7.1-alpine',
|
||||
label: 'webdevops/php-nginx:7.1-alpine'
|
||||
}
|
||||
];
|
||||
|
||||
let payload = {
|
||||
baseImage: null,
|
||||
baseBuildImage: null,
|
||||
baseImages: [],
|
||||
baseBuildImages: []
|
||||
};
|
||||
if (nodeBased.includes(buildPack)) {
|
||||
payload.baseImage = 'node:lts';
|
||||
payload.baseImages = nodeVersions;
|
||||
payload.baseBuildImage = 'node:lts';
|
||||
payload.baseBuildImages = nodeVersions;
|
||||
}
|
||||
if (staticApps.includes(buildPack)) {
|
||||
payload.baseImage = 'webdevops/nginx:alpine';
|
||||
payload.baseImages = staticVersions;
|
||||
payload.baseBuildImage = 'node:lts';
|
||||
payload.baseBuildImages = nodeVersions;
|
||||
}
|
||||
if (buildPack === 'python') {
|
||||
payload.baseImage = 'python:3-alpine';
|
||||
}
|
||||
if (buildPack === 'rust') {
|
||||
payload.baseImage = 'rust:latest';
|
||||
payload.baseBuildImage = 'rust:latest';
|
||||
payload.baseImages = rustVersions;
|
||||
payload.baseBuildImages = rustVersions;
|
||||
}
|
||||
if (buildPack === 'deno') {
|
||||
payload.baseImage = 'denoland/deno:latest';
|
||||
}
|
||||
if (buildPack === 'php') {
|
||||
payload.baseImage = 'webdevops/php-apache:8.0-alpine';
|
||||
payload.baseImages = phpVersions;
|
||||
}
|
||||
if (buildPack === 'laravel') {
|
||||
payload.baseImage = 'webdevops/php-apache:8.0-alpine';
|
||||
payload.baseBuildImage = 'node:18';
|
||||
payload.baseBuildImages = nodeVersions;
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,16 @@ import { buildImage } from '$lib/docker';
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
const createDockerfile = async (data, image): Promise<void> => {
|
||||
const { workdir, port, baseDirectory, secrets, pullmergeRequestId, denoMainFile, denoOptions } =
|
||||
data;
|
||||
const {
|
||||
workdir,
|
||||
port,
|
||||
baseDirectory,
|
||||
secrets,
|
||||
pullmergeRequestId,
|
||||
denoMainFile,
|
||||
denoOptions,
|
||||
buildId
|
||||
} = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
|
||||
let depsFound = false;
|
||||
@@ -14,7 +22,7 @@ const createDockerfile = async (data, image): Promise<void> => {
|
||||
|
||||
Dockerfile.push(`FROM ${image}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`LABEL coolify.image=true`);
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
if (secrets.length > 0) {
|
||||
secrets.forEach((secret) => {
|
||||
if (secret.isBuildSecret) {
|
||||
@@ -45,8 +53,8 @@ const createDockerfile = async (data, image): Promise<void> => {
|
||||
|
||||
export default async function (data) {
|
||||
try {
|
||||
const image = 'denoland/deno:latest';
|
||||
await createDockerfile(data, image);
|
||||
const { baseImage, baseBuildImage } = data;
|
||||
await createDockerfile(data, baseImage);
|
||||
await buildImage(data);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
|
||||
@@ -24,6 +24,7 @@ export default async function ({
|
||||
.toString()
|
||||
.trim()
|
||||
.split('\n');
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
if (secrets.length > 0) {
|
||||
secrets.forEach((secret) => {
|
||||
if (secret.isBuildSecret) {
|
||||
@@ -42,6 +43,7 @@ export default async function ({
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await fs.writeFile(`${dockerFileOut}${dockerFileLocation}`, Dockerfile.join('\n'));
|
||||
await buildImage({ applicationId, tag, workdir, docker, buildId, debug, dockerFileLocation });
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,25 +2,25 @@ import { buildCacheImageWithNode, buildImage } from '$lib/docker';
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
const createDockerfile = async (data, imageforBuild): Promise<void> => {
|
||||
const { applicationId, tag, workdir, publishDirectory } = data;
|
||||
const { applicationId, tag, workdir, publishDirectory, baseImage, buildId } = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
|
||||
Dockerfile.push(`FROM ${imageforBuild}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`LABEL coolify.image=true`);
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
|
||||
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
|
||||
if (baseImage.includes('nginx')) {
|
||||
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
|
||||
}
|
||||
Dockerfile.push(`EXPOSE 80`);
|
||||
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
|
||||
};
|
||||
|
||||
export default async function (data) {
|
||||
try {
|
||||
const image = 'webdevops/nginx:alpine';
|
||||
const imageForBuild = 'node:lts';
|
||||
|
||||
await buildCacheImageWithNode(data, imageForBuild);
|
||||
await createDockerfile(data, image);
|
||||
const { baseImage, baseBuildImage } = data;
|
||||
await buildCacheImageWithNode(data, baseImage);
|
||||
await createDockerfile(data, baseBuildImage);
|
||||
await buildImage(data);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
|
||||
@@ -14,6 +14,7 @@ import astro from './static';
|
||||
import eleventy from './static';
|
||||
import python from './python';
|
||||
import deno from './deno';
|
||||
import laravel from './laravel';
|
||||
|
||||
export {
|
||||
node,
|
||||
@@ -31,5 +32,6 @@ export {
|
||||
astro,
|
||||
eleventy,
|
||||
python,
|
||||
deno
|
||||
deno,
|
||||
laravel
|
||||
};
|
||||
|
||||
40
src/lib/buildPacks/laravel.ts
Normal file
40
src/lib/buildPacks/laravel.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { buildCacheImageForLaravel, buildImage } from '$lib/docker';
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
const createDockerfile = async (data, image): Promise<void> => {
|
||||
const { workdir, applicationId, tag, buildId } = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
|
||||
Dockerfile.push(`FROM ${image}`);
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`ENV WEB_DOCUMENT_ROOT /app/public`);
|
||||
Dockerfile.push(`COPY --chown=application:application composer.* ./`);
|
||||
Dockerfile.push(`COPY --chown=application:application database/ database/`);
|
||||
Dockerfile.push(
|
||||
`RUN composer install --ignore-platform-reqs --no-interaction --no-plugins --no-scripts --prefer-dist`
|
||||
);
|
||||
Dockerfile.push(
|
||||
`COPY --chown=application:application --from=${applicationId}:${tag}-cache /app/public/js/ /app/public/js/`
|
||||
);
|
||||
Dockerfile.push(
|
||||
`COPY --chown=application:application --from=${applicationId}:${tag}-cache /app/public/css/ /app/public/css/`
|
||||
);
|
||||
Dockerfile.push(
|
||||
`COPY --chown=application:application --from=${applicationId}:${tag}-cache /app/mix-manifest.json /app/public/mix-manifest.json`
|
||||
);
|
||||
Dockerfile.push(`COPY --chown=application:application . ./`);
|
||||
Dockerfile.push(`EXPOSE 80`);
|
||||
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
|
||||
};
|
||||
|
||||
export default async function (data) {
|
||||
const { baseImage, baseBuildImage } = data;
|
||||
try {
|
||||
await buildCacheImageForLaravel(data, baseBuildImage);
|
||||
await createDockerfile(data, baseImage);
|
||||
await buildImage(data);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,13 @@ import { buildCacheImageWithNode, buildImage } from '$lib/docker';
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
const createDockerfile = async (data, image): Promise<void> => {
|
||||
const { applicationId, tag, port, startCommand, workdir, baseDirectory } = data;
|
||||
const { buildId, applicationId, tag, port, startCommand, workdir, baseDirectory } = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
const isPnpm = startCommand.includes('pnpm');
|
||||
|
||||
Dockerfile.push(`FROM ${image}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`LABEL coolify.image=true`);
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
if (isPnpm) {
|
||||
Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm');
|
||||
Dockerfile.push('RUN pnpm add -g pnpm');
|
||||
@@ -22,11 +22,9 @@ const createDockerfile = async (data, image): Promise<void> => {
|
||||
|
||||
export default async function (data) {
|
||||
try {
|
||||
const image = 'node:lts';
|
||||
const imageForBuild = 'node:lts';
|
||||
|
||||
await buildCacheImageWithNode(data, imageForBuild);
|
||||
await createDockerfile(data, image);
|
||||
const { baseImage, baseBuildImage } = data;
|
||||
await buildCacheImageWithNode(data, baseBuildImage);
|
||||
await createDockerfile(data, baseImage);
|
||||
await buildImage(data);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { checkPnpm } from './common';
|
||||
|
||||
const createDockerfile = async (data, image): Promise<void> => {
|
||||
const {
|
||||
buildId,
|
||||
workdir,
|
||||
port,
|
||||
installCommand,
|
||||
@@ -17,7 +18,7 @@ const createDockerfile = async (data, image): Promise<void> => {
|
||||
const isPnpm = checkPnpm(installCommand, buildCommand, startCommand);
|
||||
Dockerfile.push(`FROM ${image}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`LABEL coolify.image=true`);
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
if (secrets.length > 0) {
|
||||
secrets.forEach((secret) => {
|
||||
if (secret.isBuildSecret) {
|
||||
@@ -50,8 +51,8 @@ const createDockerfile = async (data, image): Promise<void> => {
|
||||
|
||||
export default async function (data) {
|
||||
try {
|
||||
const image = 'node:lts';
|
||||
await createDockerfile(data, image);
|
||||
const { baseImage, baseBuildImage } = data;
|
||||
await createDockerfile(data, baseImage);
|
||||
await buildImage(data);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
|
||||
@@ -11,14 +11,15 @@ const createDockerfile = async (data, image): Promise<void> => {
|
||||
startCommand,
|
||||
baseDirectory,
|
||||
secrets,
|
||||
pullmergeRequestId
|
||||
pullmergeRequestId,
|
||||
buildId
|
||||
} = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
const isPnpm = checkPnpm(installCommand, buildCommand, startCommand);
|
||||
|
||||
Dockerfile.push(`FROM ${image}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`LABEL coolify.image=true`);
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
if (secrets.length > 0) {
|
||||
secrets.forEach((secret) => {
|
||||
if (secret.isBuildSecret) {
|
||||
@@ -50,8 +51,8 @@ const createDockerfile = async (data, image): Promise<void> => {
|
||||
|
||||
export default async function (data) {
|
||||
try {
|
||||
const image = 'node:lts';
|
||||
await createDockerfile(data, image);
|
||||
const { baseImage, baseBuildImage } = data;
|
||||
await createDockerfile(data, baseImage);
|
||||
await buildImage(data);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
|
||||
@@ -11,13 +11,14 @@ const createDockerfile = async (data, image): Promise<void> => {
|
||||
startCommand,
|
||||
baseDirectory,
|
||||
secrets,
|
||||
pullmergeRequestId
|
||||
pullmergeRequestId,
|
||||
buildId
|
||||
} = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
const isPnpm = checkPnpm(installCommand, buildCommand, startCommand);
|
||||
Dockerfile.push(`FROM ${image}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`LABEL coolify.image=true`);
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
if (secrets.length > 0) {
|
||||
secrets.forEach((secret) => {
|
||||
if (secret.isBuildSecret) {
|
||||
@@ -49,8 +50,8 @@ const createDockerfile = async (data, image): Promise<void> => {
|
||||
|
||||
export default async function (data) {
|
||||
try {
|
||||
const image = 'node:lts';
|
||||
await createDockerfile(data, image);
|
||||
const { baseImage, baseBuildImage } = data;
|
||||
await createDockerfile(data, baseImage);
|
||||
await buildImage(data);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { buildImage } from '$lib/docker';
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
|
||||
const { workdir, baseDirectory } = data;
|
||||
const { workdir, baseDirectory, buildId } = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
let composerFound = false;
|
||||
try {
|
||||
@@ -11,7 +11,7 @@ const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
|
||||
} catch (error) {}
|
||||
|
||||
Dockerfile.push(`FROM ${image}`);
|
||||
Dockerfile.push(`LABEL coolify.image=true`);
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`COPY .${baseDirectory || ''} /app`);
|
||||
if (htaccessFound) {
|
||||
@@ -27,7 +27,7 @@ const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
|
||||
};
|
||||
|
||||
export default async function (data) {
|
||||
const { workdir, baseDirectory } = data;
|
||||
const { workdir, baseDirectory, baseImage } = data;
|
||||
try {
|
||||
let htaccessFound = false;
|
||||
try {
|
||||
@@ -36,10 +36,7 @@ export default async function (data) {
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
const image = htaccessFound
|
||||
? 'webdevops/php-apache:8.0-alpine'
|
||||
: 'webdevops/php-nginx:8.0-alpine';
|
||||
await createDockerfile(data, image, htaccessFound);
|
||||
await createDockerfile(data, baseImage, htaccessFound);
|
||||
await buildImage(data);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
|
||||
@@ -10,12 +10,13 @@ const createDockerfile = async (data, image): Promise<void> => {
|
||||
pullmergeRequestId,
|
||||
pythonWSGI,
|
||||
pythonModule,
|
||||
pythonVariable
|
||||
pythonVariable,
|
||||
buildId
|
||||
} = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
Dockerfile.push(`FROM ${image}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`LABEL coolify.image=true`);
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
if (secrets.length > 0) {
|
||||
secrets.forEach((secret) => {
|
||||
if (secret.isBuildSecret) {
|
||||
@@ -62,8 +63,8 @@ const createDockerfile = async (data, image): Promise<void> => {
|
||||
|
||||
export default async function (data) {
|
||||
try {
|
||||
const image = 'python:3-alpine';
|
||||
await createDockerfile(data, image);
|
||||
const { baseImage, baseBuildImage } = data;
|
||||
await createDockerfile(data, baseImage);
|
||||
await buildImage(data);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
|
||||
@@ -2,24 +2,25 @@ import { buildCacheImageWithNode, buildImage } from '$lib/docker';
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
const createDockerfile = async (data, image): Promise<void> => {
|
||||
const { applicationId, tag, workdir, publishDirectory } = data;
|
||||
const { applicationId, tag, workdir, publishDirectory, baseImage, buildId } = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
|
||||
Dockerfile.push(`FROM ${image}`);
|
||||
Dockerfile.push(`LABEL coolify.image=true`);
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
|
||||
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
|
||||
if (baseImage.includes('nginx')) {
|
||||
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
|
||||
}
|
||||
Dockerfile.push(`EXPOSE 80`);
|
||||
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
|
||||
};
|
||||
|
||||
export default async function (data) {
|
||||
try {
|
||||
const image = 'webdevops/nginx:alpine';
|
||||
const imageForBuild = 'node:lts';
|
||||
await buildCacheImageWithNode(data, imageForBuild);
|
||||
await createDockerfile(data, image);
|
||||
const { baseImage, baseBuildImage } = data;
|
||||
await buildCacheImageWithNode(data, baseBuildImage);
|
||||
await createDockerfile(data, baseImage);
|
||||
await buildImage(data);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
|
||||
@@ -4,11 +4,11 @@ import { promises as fs } from 'fs';
|
||||
import TOML from '@iarna/toml';
|
||||
|
||||
const createDockerfile = async (data, image, name): Promise<void> => {
|
||||
const { workdir, port, applicationId, tag } = data;
|
||||
const { workdir, port, applicationId, tag, buildId } = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
Dockerfile.push(`FROM ${image}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`LABEL coolify.image=true`);
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/target target`);
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/local/cargo /usr/local/cargo`);
|
||||
Dockerfile.push(`COPY . .`);
|
||||
@@ -27,14 +27,12 @@ const createDockerfile = async (data, image, name): Promise<void> => {
|
||||
|
||||
export default async function (data) {
|
||||
try {
|
||||
const { workdir } = data;
|
||||
const image = 'rust:latest';
|
||||
const imageForBuild = 'rust:latest';
|
||||
const { workdir, baseImage, baseBuildImage } = data;
|
||||
const { stdout: cargoToml } = await asyncExecShell(`cat ${workdir}/Cargo.toml`);
|
||||
const parsedToml: any = TOML.parse(cargoToml);
|
||||
const name = parsedToml.package.name;
|
||||
await buildCacheImageWithCargo(data, imageForBuild);
|
||||
await createDockerfile(data, image, name);
|
||||
await buildCacheImageWithCargo(data, baseBuildImage);
|
||||
await createDockerfile(data, baseImage, name);
|
||||
await buildImage(data);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
|
||||
@@ -10,13 +10,15 @@ const createDockerfile = async (data, image): Promise<void> => {
|
||||
baseDirectory,
|
||||
publishDirectory,
|
||||
secrets,
|
||||
pullmergeRequestId
|
||||
pullmergeRequestId,
|
||||
baseImage,
|
||||
buildId
|
||||
} = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
|
||||
Dockerfile.push(`FROM ${image}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`LABEL coolify.image=true`);
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
if (secrets.length > 0) {
|
||||
secrets.forEach((secret) => {
|
||||
if (secret.isBuildSecret) {
|
||||
@@ -37,17 +39,18 @@ const createDockerfile = async (data, image): Promise<void> => {
|
||||
} else {
|
||||
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
|
||||
}
|
||||
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
|
||||
if (baseImage.includes('nginx')) {
|
||||
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
|
||||
}
|
||||
Dockerfile.push(`EXPOSE 80`);
|
||||
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
|
||||
};
|
||||
|
||||
export default async function (data) {
|
||||
try {
|
||||
const image = 'webdevops/nginx:alpine';
|
||||
const imageForBuild = 'node:lts';
|
||||
if (data.buildCommand) await buildCacheImageWithNode(data, imageForBuild);
|
||||
await createDockerfile(data, image);
|
||||
const { baseImage, baseBuildImage } = data;
|
||||
if (data.buildCommand) await buildCacheImageWithNode(data, baseBuildImage);
|
||||
await createDockerfile(data, baseImage);
|
||||
await buildImage(data);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
|
||||
@@ -2,25 +2,25 @@ import { buildCacheImageWithNode, buildImage } from '$lib/docker';
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
const createDockerfile = async (data, image): Promise<void> => {
|
||||
const { applicationId, tag, workdir, publishDirectory } = data;
|
||||
const { applicationId, tag, workdir, publishDirectory, baseImage, buildId } = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
|
||||
Dockerfile.push(`FROM ${image}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`LABEL coolify.image=true`);
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
|
||||
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
|
||||
if (baseImage.includes('nginx')) {
|
||||
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
|
||||
}
|
||||
Dockerfile.push(`EXPOSE 80`);
|
||||
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
|
||||
};
|
||||
|
||||
export default async function (data) {
|
||||
try {
|
||||
const image = 'webdevops/nginx:alpine';
|
||||
const imageForBuild = 'node:lts';
|
||||
|
||||
await buildCacheImageWithNode(data, imageForBuild);
|
||||
await createDockerfile(data, image);
|
||||
const { baseImage, baseBuildImage } = data;
|
||||
await buildCacheImageWithNode(data, baseBuildImage);
|
||||
await createDockerfile(data, baseImage);
|
||||
await buildImage(data);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
|
||||
@@ -2,24 +2,25 @@ import { buildCacheImageWithNode, buildImage } from '$lib/docker';
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
const createDockerfile = async (data, image): Promise<void> => {
|
||||
const { applicationId, tag, workdir, publishDirectory } = data;
|
||||
const { applicationId, tag, workdir, publishDirectory, baseImage, buildId } = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
|
||||
Dockerfile.push(`FROM ${image}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`LABEL coolify.image=true`);
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
|
||||
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
|
||||
if (baseImage.includes('nginx')) {
|
||||
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
|
||||
}
|
||||
Dockerfile.push(`EXPOSE 80`);
|
||||
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
|
||||
};
|
||||
|
||||
export default async function (data) {
|
||||
try {
|
||||
const image = 'webdevops/nginx:alpine';
|
||||
const imageForBuild = 'node:lts';
|
||||
await buildCacheImageWithNode(data, imageForBuild);
|
||||
await createDockerfile(data, image);
|
||||
const { baseImage, baseBuildImage } = data;
|
||||
await buildCacheImageWithNode(data, baseBuildImage);
|
||||
await createDockerfile(data, baseImage);
|
||||
await buildImage(data);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
|
||||
@@ -26,7 +26,7 @@ try {
|
||||
initialScope: {
|
||||
tags: {
|
||||
appId: process.env['COOLIFY_APP_ID'],
|
||||
'os.arch': os.arch(),
|
||||
'os.arch': getOsArch(),
|
||||
'os.platform': os.platform(),
|
||||
'os.release': os.release()
|
||||
}
|
||||
@@ -175,3 +175,7 @@ export function generateTimestamp(): string {
|
||||
export function getDomain(domain: string): string {
|
||||
return domain?.replace('https://', '').replace('http://', '');
|
||||
}
|
||||
|
||||
export function getOsArch() {
|
||||
return os.arch();
|
||||
}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
<script>
|
||||
export let service;
|
||||
import Ghost from './svg/services/Ghost.svelte';
|
||||
import Hasura from './svg/services/Hasura.svelte';
|
||||
import LanguageTool from './svg/services/LanguageTool.svelte';
|
||||
import MinIo from './svg/services/MinIO.svelte';
|
||||
import N8n from './svg/services/N8n.svelte';
|
||||
import NocoDb from './svg/services/NocoDB.svelte';
|
||||
import PlausibleAnalytics from './svg/services/PlausibleAnalytics.svelte';
|
||||
import Umami from './svg/services/Umami.svelte';
|
||||
import UptimeKuma from './svg/services/UptimeKuma.svelte';
|
||||
import VaultWarden from './svg/services/VaultWarden.svelte';
|
||||
import VsCodeServer from './svg/services/VSCodeServer.svelte';
|
||||
import Wordpress from './svg/services/Wordpress.svelte';
|
||||
import Fider from './svg/services/Fider.svelte';
|
||||
</script>
|
||||
|
||||
{#if service.type === 'plausibleanalytics'}
|
||||
@@ -52,4 +55,16 @@
|
||||
<a href="https://ghost.org" target="_blank">
|
||||
<Ghost />
|
||||
</a>
|
||||
{:else if service.type === 'umami'}
|
||||
<a href="https://umami.is" target="_blank">
|
||||
<Umami />
|
||||
</a>
|
||||
{:else if service.type === 'hasura'}
|
||||
<a href="https://hasura.io" target="_blank">
|
||||
<Hasura />
|
||||
</a>
|
||||
{:else if service.type === 'fider'}
|
||||
<a href="https://fider.io" target="_blank">
|
||||
<Fider />
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
@@ -19,7 +19,7 @@ export const staticDeployments = [
|
||||
'astro',
|
||||
'eleventy'
|
||||
];
|
||||
export const notNodeDeployments = ['php', 'docker', 'rust', 'python', 'deno'];
|
||||
export const notNodeDeployments = ['php', 'docker', 'rust', 'python', 'deno', 'laravel'];
|
||||
|
||||
export function getDomain(domain) {
|
||||
return domain?.replace('https://', '').replace('http://', '');
|
||||
@@ -180,5 +180,38 @@ export const supportedServiceTypesAndVersions = [
|
||||
ports: {
|
||||
main: 7700
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'umami',
|
||||
fancyName: 'Umami',
|
||||
baseImage: 'ghcr.io/mikecao/umami',
|
||||
images: ['postgres:12-alpine'],
|
||||
versions: ['postgresql-latest'],
|
||||
recommendedVersion: 'postgresql-latest',
|
||||
ports: {
|
||||
main: 3000
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'hasura',
|
||||
fancyName: 'Hasura',
|
||||
baseImage: 'hasura/graphql-engine',
|
||||
images: ['postgres:12-alpine'],
|
||||
versions: ['latest', 'v2.5.1'],
|
||||
recommendedVersion: 'v2.5.1',
|
||||
ports: {
|
||||
main: 8080
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'fider',
|
||||
fancyName: 'Fider',
|
||||
baseImage: 'getfider/fider',
|
||||
images: ['postgres:12-alpine'],
|
||||
versions: ['stable'],
|
||||
recommendedVersion: 'stable',
|
||||
ports: {
|
||||
main: 3000
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
10
src/lib/components/svg/applications/Laravel.svelte
Normal file
10
src/lib/components/svg/applications/Laravel.svelte
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg
|
||||
class="absolute top-0 left-0 -m-4 h-10 w-10"
|
||||
viewBox="0 0 50 52"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><title>Logomark</title><path
|
||||
d="M49.626 11.564a.809.809 0 0 1 .028.209v10.972a.8.8 0 0 1-.402.694l-9.209 5.302V39.25c0 .286-.152.55-.4.694L20.42 51.01c-.044.025-.092.041-.14.058-.018.006-.035.017-.054.022a.805.805 0 0 1-.41 0c-.022-.006-.042-.018-.063-.026-.044-.016-.09-.03-.132-.054L.402 39.944A.801.801 0 0 1 0 39.25V6.334c0-.072.01-.142.028-.21.006-.023.02-.044.028-.067.015-.042.029-.085.051-.124.015-.026.037-.047.055-.071.023-.032.044-.065.071-.093.023-.023.053-.04.079-.06.029-.024.055-.05.088-.069h.001l9.61-5.533a.802.802 0 0 1 .8 0l9.61 5.533h.002c.032.02.059.045.088.068.026.02.055.038.078.06.028.029.048.062.072.094.017.024.04.045.054.071.023.04.036.082.052.124.008.023.022.044.028.068a.809.809 0 0 1 .028.209v20.559l8.008-4.611v-10.51c0-.07.01-.141.028-.208.007-.024.02-.045.028-.068.016-.042.03-.085.052-.124.015-.026.037-.047.054-.071.024-.032.044-.065.072-.093.023-.023.052-.04.078-.06.03-.024.056-.05.088-.069h.001l9.611-5.533a.801.801 0 0 1 .8 0l9.61 5.533c.034.02.06.045.09.068.025.02.054.038.077.06.028.029.048.062.072.094.018.024.04.045.054.071.023.039.036.082.052.124.009.023.022.044.028.068zm-1.574 10.718v-9.124l-3.363 1.936-4.646 2.675v9.124l8.01-4.611zm-9.61 16.505v-9.13l-4.57 2.61-13.05 7.448v9.216l17.62-10.144zM1.602 7.719v31.068L19.22 48.93v-9.214l-9.204-5.209-.003-.002-.004-.002c-.031-.018-.057-.044-.086-.066-.025-.02-.054-.036-.076-.058l-.002-.003c-.026-.025-.044-.056-.066-.084-.02-.027-.044-.05-.06-.078l-.001-.003c-.018-.03-.029-.066-.042-.1-.013-.03-.03-.058-.038-.09v-.001c-.01-.038-.012-.078-.016-.117-.004-.03-.012-.06-.012-.09v-.002-21.481L4.965 9.654 1.602 7.72zm8.81-5.994L2.405 6.334l8.005 4.609 8.006-4.61-8.006-4.608zm4.164 28.764l4.645-2.674V7.719l-3.363 1.936-4.646 2.675v20.096l3.364-1.937zM39.243 7.164l-8.006 4.609 8.006 4.609 8.005-4.61-8.005-4.608zm-.801 10.605l-4.646-2.675-3.363-1.936v9.124l4.645 2.674 3.364 1.937v-9.124zM20.02 38.33l11.743-6.704 5.87-3.35-8-4.606-9.211 5.303-8.395 4.833 7.993 4.524z"
|
||||
fill="#FF2D20"
|
||||
fill-rule="evenodd"
|
||||
/></svg
|
||||
>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -4,9 +4,7 @@
|
||||
|
||||
<svg
|
||||
class={isAbsolute ? 'absolute top-0 left-0 -m-5 h-10 w-10' : 'mx-auto w-8 h-8'}
|
||||
height="64"
|
||||
viewBox="0 0 32 32"
|
||||
width="64"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
><defs
|
||||
|
||||
121
src/lib/components/svg/services/Fider.svelte
Normal file
121
src/lib/components/svg/services/Fider.svelte
Normal file
@@ -0,0 +1,121 @@
|
||||
<script lang="ts">
|
||||
export let isAbsolute = false;
|
||||
</script>
|
||||
|
||||
<svg
|
||||
viewBox="0 0 700 240"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={isAbsolute ? 'w-36 absolute top-0 left-0 -m-3 -mt-5' : 'w-28 mx-auto'}
|
||||
><path fill="#FDBC3D" d="m90.694 107.498-.981.39-20.608 8.23 6.332 6.547z" /><path
|
||||
fill="#8EC63F"
|
||||
d="M61.139 77.914 46.632 93 56.9 103.547c8.649-7.169 17.832-10.502 18.653-10.789L61.139 77.914z"
|
||||
/><path fill="#208ECB" d="M61.139 77.914 46.367 63.247l-14.228 14.8 14.493 14.952z" /><path
|
||||
fill="#273C8B"
|
||||
d="m40.767 57.48-6.943 2.79a38.381 38.381 0 0 0-11.742 7.418L32.14 78.047l14.228-14.8-5.601-5.768z"
|
||||
/><path
|
||||
fill="#EE4649"
|
||||
d="m119.074 138.128-.243-.25-5.653 5.675c1.897-1.516 4.287-3.66 5.896-5.425z"
|
||||
/><path
|
||||
fill="#F6944E"
|
||||
d="m102.088 150.087 3.709-1.875a46.26 46.26 0 0 0 7.381-4.659l5.653-5.676-14.311-15.285-14.493 15.072 12.061 12.423z"
|
||||
/><path fill="#FFC951" d="m90.279 107.926-14.842 14.74 14.589 14.998 14.493-15.072z" /><path
|
||||
fill="#F6CC18"
|
||||
d="m69.087 116.125-11.256 4.493c-3.301.973-6.096 2.843-8.434 5.081l11.548 11.892 14.493-14.926-6.35-6.54z"
|
||||
/><path
|
||||
fill="#C5D82D"
|
||||
d="m56.886 103.559-10.253-10.56L32 107.926l11.784 11.991c3.304-6.888 8.174-12.272 13.103-16.358z"
|
||||
/><path fill="#0D77B3" d="m32.14 78.047-14.507 14.94 14.365 14.939 14.634-14.927z" /><path
|
||||
fill="#2A377E"
|
||||
d="M32.14 78.047 22.08 67.688a38.573 38.573 0 0 0-11.093 18.455l6.645 6.843 14.506-14.94z"
|
||||
/><path
|
||||
fill="#DA2128"
|
||||
d="m94.826 162.454-4.87 5.017 14.808 15.397c-.632-1.942-1.606-4.438-2.58-6.307l-7.357-14.107z"
|
||||
/><path
|
||||
fill="#F8A561"
|
||||
d="m91.24 155.575 10.832-5.48-12.046-12.43-14.506 14.939 14.436 14.867 4.87-5.017z"
|
||||
/><path fill="#FDBC3D" d="m75.437 122.665-14.493 14.926 14.576 15.013 14.506-14.94z" /><path
|
||||
fill="#FAD412"
|
||||
d="M49.397 125.7c-6.71 6.472-9.664 16.047-9.664 16.047-.3-4.606.06-8.83.907-12.698l-8.513 8.742 14.311 14.74 14.506-14.94-11.547-11.892z"
|
||||
/><path
|
||||
fill="#C4D52D"
|
||||
d="m43.783 119.917-11.785-11.991-13.29 13.687 3.708 6.178 9.71 10 8.52-8.775a42.699 42.699 0 0 1 3.137-9.099z"
|
||||
/><path
|
||||
fill="#1B80C1"
|
||||
d="m17.633 92.986-7.638 7.72c.65 5.1 2.35 10.3 5.193 15.04l3.52 5.867 13.29-13.687-14.365-14.94z"
|
||||
/><path
|
||||
fill="#1A4685"
|
||||
d="M10.989 86.143c-1.22 4.667-1.597 9.683-.993 14.563l7.638-7.72-6.645-6.843z"
|
||||
/><path
|
||||
fill="#B12026"
|
||||
d="m89.956 197.35 12.502 13.022c4.143-8.355 5.148-18.255 2.307-27.504l-.302-.311-14.507 14.793z"
|
||||
/><path fill="#E42028" d="M89.956 167.47 75.52 182.484l14.436 14.867 14.506-14.793z" /><path
|
||||
fill="#F16B4E"
|
||||
d="m75.52 152.604-14.576 14.867 14.576 15.012 14.436-15.012z"
|
||||
/><path fill="#FAD412" d="m60.944 137.591-14.506 14.94 14.506 14.94 14.576-14.867z" /><path
|
||||
fill="#FFC951"
|
||||
d="m32.127 137.792-2.293 2.36 10.933 18.22 5.671-5.841z"
|
||||
/><path fill="#FFC951" d="m22.416 127.79 7.418 12.363 2.293-2.361z" /><path
|
||||
fill="#981C20"
|
||||
d="M102.458 210.371 89.955 197.35 75.45 212.29l12.918 13.304a36.951 36.951 0 0 0 14.09-15.222z"
|
||||
/><path
|
||||
fill="#C92039"
|
||||
d="m75.52 182.483-12.59 12.823 6.423 10.704 6.097 6.28 14.506-14.94z"
|
||||
/><path fill="#F05B41" d="m60.944 167.47-9.096 9.369 11.081 18.467 12.59-12.823z" /><path
|
||||
fill="#F6CC18"
|
||||
d="m46.438 152.53-5.671 5.842 11.081 18.467 9.096-9.368z"
|
||||
/><path
|
||||
fill="#7A1319"
|
||||
d="m74.01 213.772 8.904 14.838 4.104-2.237c.429-.233.934-.533 1.35-.78L75.45 212.29l-1.44 1.482z"
|
||||
/><path fill="#981C20" d="m69.353 206.01 4.658 7.762 1.44-1.482z" /><path
|
||||
fill="#15796E"
|
||||
d="m147.842 48.094 10.653-10.971a41.81 41.81 0 0 0 .943-6.94l-11.414-11.755-14.48 14.94 14.298 14.726z"
|
||||
/><path fill="#29B364" d="m133.53 33.354 14.494-14.926-2.737-2.965-20.95 8.422z" /><path
|
||||
fill="#21A29F"
|
||||
d="M151.819 52.189c3.057-4.334 5.434-9.932 6.677-15.066l-10.653 10.971 3.976 4.095z"
|
||||
/><path
|
||||
fill="#12827F"
|
||||
d="M159.438 30.183c.307-6.28-.783-12.862-3.488-19.006l-1.41.567-6.516 6.684 11.414 11.755zM154.54 11.744l-9.253 3.72 2.737 2.964z"
|
||||
/><path fill="#0C6355" d="m133.336 63.034 14.506-14.94-14.311-14.713-14.493 14.926z" /><path
|
||||
fill="#1B974D"
|
||||
d="m104.532 33.368 14.506 14.94 14.48-14.94-9.2-9.476-17.363 6.98z"
|
||||
/><path fill="#16669F" d="m106.955 30.872-3.485 1.401 1.062 1.095z" /><path
|
||||
fill="#44BFBD"
|
||||
d="M135.9 65.674A41.696 41.696 0 0 0 151.82 52.19l-3.977-4.095-14.506 14.94 2.564 2.64z"
|
||||
/><path
|
||||
fill="#0D5650"
|
||||
d="m115.71 74.76 11.052-4.956 6.574-6.77-14.298-14.727-14.506 14.94z"
|
||||
/><path fill="#3FAF49" d="m119.038 48.307-14.506-14.94-14.576 14.868 14.563 14.999z" /><path
|
||||
fill="#0D77B3"
|
||||
d="m104.532 33.368-1.062-1.095-20.97 8.43 7.456 7.532z"
|
||||
/><path
|
||||
fill="#0C6355"
|
||||
d="M134.766 66.217c.352-.157.789-.376 1.134-.543l-2.564-2.64-6.574 6.77 8.004-3.587z"
|
||||
/><path fill="#12827F" d="m115.71 74.76-11.178-11.513-14.506 14.94 5.47 5.633z" /><path
|
||||
fill="#4EB648"
|
||||
d="M104.532 63.247 89.956 48.235 75.52 63.247l14.493 14.927z"
|
||||
/><path fill="#16669F" d="M89.956 48.235 82.5 40.703l-20.868 8.388L75.52 63.247z" /><path
|
||||
fill="#FBB139"
|
||||
d="M129.526 119.012c1.902-7.144 2.108-15.019.353-22.538l-11.048 11.379 10.695 11.16z"
|
||||
/><path
|
||||
fill="#E2B523"
|
||||
d="m110.62 99.542 8.21 8.311 11.049-11.38a46.303 46.303 0 0 0-1.186-4.149l-18.074 7.218z"
|
||||
/><path fill="#189590" d="M90.026 78.186 76.128 92.501l19.367-8.681z" /><path
|
||||
fill="#8EC63F"
|
||||
d="m76.083 92.521 13.943-14.335-14.506-14.94-14.381 14.668 14.413 14.844z"
|
||||
/><path
|
||||
fill="#0D77B3"
|
||||
d="M75.52 63.247 61.633 49.09l-2.264.91-13.002 13.246L61.14 77.914z"
|
||||
/><path fill="#1953A2" d="m59.37 50.002-18.603 7.477 5.6 5.768z" /><path
|
||||
fill="#ED3551"
|
||||
d="M119.324 137.84c.885-.988 2.15-2.59 2.942-3.646l-3.17 3.41.228.236z"
|
||||
/><path
|
||||
fill="#F8A561"
|
||||
d="m118.83 137.877 3.437-3.683a46.268 46.268 0 0 0 7.259-15.182l-10.695-11.159-14.311 14.74 14.31 15.284z"
|
||||
/><path
|
||||
fill="#E9B520"
|
||||
d="m90.279 107.926 14.24 14.666 14.312-14.739-8.212-8.311-19.925 7.956z"
|
||||
/><path
|
||||
fill="#EE4649"
|
||||
d="m118.83 137.877.244.251c.085-.095.166-.193.25-.288l-.228-.235-.265.272z"
|
||||
/></svg
|
||||
>
|
||||
26
src/lib/components/svg/services/Hasura.svelte
Normal file
26
src/lib/components/svg/services/Hasura.svelte
Normal file
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
export let isAbsolute = false;
|
||||
</script>
|
||||
|
||||
<svg
|
||||
class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 mx-auto'}
|
||||
viewBox="0 0 81 84"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_5273_21928)">
|
||||
<path
|
||||
d="M79.7186 28.6019C82.1218 21.073 80.6778 6.03601 76.0158 0.487861C75.4073 -0.238064 74.2624 -0.134361 73.757 0.664158L68.0121 9.72786C66.5887 11.5427 64.0308 11.9575 62.1124 10.6923C55.8827 6.59601 48.4359 4.21082 40.4322 4.21082C32.4285 4.21082 24.9817 6.59601 18.752 10.6923C16.8336 11.9575 14.2757 11.5323 12.8523 9.72786L7.10738 0.664158C6.60199 -0.134361 5.45712 -0.238064 4.84859 0.487861C0.186621 6.03601 -1.25735 21.073 1.14583 28.6019C1.94002 31.1012 2.16693 33.7456 1.69248 36.3279C1.22834 38.879 0.753897 41.9693 0.753897 44.1056C0.753897 66.1323 18.5251 84.0004 40.4322 84.0004C62.3497 84.0004 80.1105 66.1427 80.1105 44.1056C80.1105 41.959 79.6464 38.879 79.1719 36.3279C78.6975 33.7456 78.9244 31.1012 79.7186 28.6019ZM40.4322 75.0819C23.4965 75.0819 9.71684 61.2271 9.71684 44.199C9.71684 43.639 9.73747 43.0893 9.7581 42.5397C10.3769 30.9353 17.3802 21.0108 27.3024 16.2819C31.2836 14.3738 35.7393 13.316 40.4322 13.316C45.1251 13.316 49.5808 14.3842 53.5724 16.2923C63.4945 21.0212 70.4978 30.9456 71.1166 42.5397C71.1476 43.0893 71.1579 43.639 71.1579 44.199C71.1476 61.2271 57.3679 75.0819 40.4322 75.0819Z"
|
||||
fill="#1EB4D4"
|
||||
/>
|
||||
<path
|
||||
d="M53.7371 56.083L45.8881 42.4044L39.153 30.997C38.9983 30.7274 38.7095 30.5615 38.3898 30.5615H31.9538C31.634 30.5615 31.3452 30.7378 31.1905 31.0074C31.0358 31.2874 31.0358 31.6296 31.2008 31.8993L37.6368 42.7881L28.9936 56.0415C28.8183 56.3111 28.7977 56.6637 28.9524 56.9541C29.1071 57.2444 29.4062 57.4207 29.7259 57.4207H36.2032C36.5023 57.4207 36.7808 57.2652 36.9458 57.0163L41.6181 49.6741L45.8056 56.9748C45.9603 57.2548 46.2594 57.4207 46.5688 57.4207H52.9533C53.273 57.4207 53.5618 57.2548 53.7165 56.9748C53.9022 56.6948 53.9022 56.363 53.7371 56.083Z"
|
||||
fill="#1EB4D4"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_5273_21928">
|
||||
<rect width="81" height="84" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
83
src/lib/components/svg/services/Umami.svelte
Normal file
83
src/lib/components/svg/services/Umami.svelte
Normal file
@@ -0,0 +1,83 @@
|
||||
<script lang="ts">
|
||||
export let isAbsolute = false;
|
||||
</script>
|
||||
|
||||
<svg
|
||||
version="1.0"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 856.000000 856.000000"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 mx-auto'}
|
||||
>
|
||||
<metadata> Created by potrace 1.11, written by Peter Selinger 2001-2013 </metadata>
|
||||
<g
|
||||
transform="translate(0.000000,856.000000) scale(0.100000,-0.100000)"
|
||||
fill="currentColor"
|
||||
stroke="none"
|
||||
>
|
||||
<path
|
||||
d="M4027 8163 c-2 -2 -28 -5 -58 -7 -50 -4 -94 -9 -179 -22 -19 -2 -48
|
||||
-6 -65 -9 -47 -6 -236 -44 -280 -55 -22 -6 -49 -12 -60 -15 -34 -6 -58 -13
|
||||
-130 -36 -38 -13 -72 -23 -75 -24 -29 -6 -194 -66 -264 -96 -49 -22 -95 -39
|
||||
-102 -39 -7 0 -19 -7 -28 -15 -8 -9 -18 -15 -21 -14 -7 1 -197 -92 -205 -101
|
||||
-3 -3 -21 -13 -40 -24 -79 -42 -123 -69 -226 -137 -94 -62 -246 -173 -280
|
||||
-204 -6 -5 -29 -25 -52 -43 -136 -111 -329 -305 -457 -462 -21 -25 -41 -47
|
||||
-44 -50 -4 -3 -22 -26 -39 -52 -18 -25 -38 -52 -45 -60 -34 -35 -207 -308
|
||||
-259 -408 -13 -25 -25 -47 -28 -50 -11 -11 -121 -250 -159 -346 -42 -105 -114
|
||||
-321 -126 -374 l-7 -30 -263 0 c-245 0 -268 -2 -321 -21 -94 -35 -171 -122
|
||||
-191 -216 -9 -39 -8 -852 0 -938 9 -87 16 -150 23 -195 3 -19 6 -48 8 -65 3
|
||||
-29 14 -97 22 -140 3 -11 7 -36 10 -55 3 -19 9 -51 14 -70 5 -19 11 -46 14
|
||||
-60 29 -138 104 -401 145 -505 5 -11 23 -58 42 -105 18 -47 42 -105 52 -130
|
||||
11 -25 21 -49 22 -55 3 -10 109 -224 164 -330 18 -33 50 -89 71 -124 22 -34
|
||||
40 -64 40 -66 0 -8 104 -161 114 -167 6 -4 7 -8 3 -8 -4 0 4 -12 18 -27 14
|
||||
-15 25 -32 25 -36 0 -5 6 -14 13 -21 6 -7 21 -25 32 -41 11 -15 34 -44 50 -64
|
||||
17 -21 41 -52 55 -70 13 -18 33 -43 45 -56 11 -13 42 -49 70 -81 100 -118 359
|
||||
-369 483 -469 34 -27 62 -53 62 -57 0 -5 6 -8 13 -8 7 0 19 -9 27 -20 8 -11
|
||||
19 -20 26 -20 6 0 19 -9 29 -20 10 -11 22 -20 27 -20 5 0 23 -13 41 -30 18
|
||||
-16 37 -30 44 -30 6 0 13 -4 15 -8 3 -8 186 -132 194 -132 2 0 27 -15 56 -34
|
||||
132 -83 377 -207 558 -280 36 -15 74 -31 85 -36 62 -26 220 -81 320 -109 79
|
||||
-23 191 -53 214 -57 14 -3 28 -7 31 -9 4 -2 20 -7 36 -9 16 -3 40 -8 54 -11
|
||||
14 -3 36 -8 50 -11 14 -2 36 -7 50 -10 13 -3 40 -8 60 -10 19 -2 46 -7 60 -10
|
||||
54 -10 171 -25 320 -40 90 -9 613 -12 636 -4 11 5 28 4 37 -1 9 -6 17 -6 17
|
||||
-1 0 4 10 8 23 9 29 0 154 12 192 18 17 3 46 7 65 9 70 10 131 20 183 32 16 3
|
||||
38 7 50 9 45 7 165 36 252 60 50 14 100 28 112 30 12 3 34 10 48 15 14 5 25 7
|
||||
25 4 0 -4 6 -2 13 3 6 6 30 16 52 22 22 7 47 15 55 18 8 4 17 7 20 7 10 2 179
|
||||
68 240 94 96 40 342 159 395 191 17 10 53 30 80 46 28 15 81 47 118 71 37 24
|
||||
72 44 76 44 5 0 11 3 13 8 2 4 30 25 63 47 33 22 62 42 65 45 3 3 50 38 105
|
||||
79 55 40 105 79 110 85 6 6 24 22 40 34 85 65 465 430 465 447 0 3 8 13 18 23
|
||||
9 10 35 40 57 66 22 27 47 56 55 65 8 9 42 52 74 96 32 44 71 96 85 115 140
|
||||
183 358 576 461 830 12 30 28 69 36 85 24 56 123 355 117 355 -3 0 -1 6 5 13
|
||||
6 6 14 30 18 52 10 48 9 46 17 65 5 13 37 155 52 230 9 42 35 195 40 231 34
|
||||
235 40 357 40 804 l0 420 -24 44 c-46 87 -143 157 -231 166 -19 2 -144 4 -276
|
||||
4 l-242 1 -36 118 c-21 64 -46 139 -56 166 -11 27 -20 52 -20 57 0 5 -11 33
|
||||
-25 63 -14 30 -25 58 -25 61 0 18 -152 329 -162 333 -5 2 -8 10 -8 18 0 8 -4
|
||||
14 -10 14 -5 0 -9 3 -8 8 3 9 -40 82 -128 217 -63 97 -98 145 -187 259 -133
|
||||
171 -380 420 -559 564 -71 56 -132 102 -138 102 -5 0 -10 3 -10 8 0 4 -25 23
|
||||
-55 42 -30 19 -55 38 -55 43 0 4 -6 7 -13 7 -7 0 -22 8 -33 18 -11 9 -37 26
|
||||
-59 37 -21 11 -44 25 -50 30 -41 37 -413 220 -540 266 -27 9 -61 22 -75 27
|
||||
-14 5 -28 10 -32 11 -4 1 -28 10 -53 21 -25 11 -46 19 -48 18 -2 -1 -109 29
|
||||
-137 40 -13 4 -32 9 -65 16 -5 1 -16 5 -22 9 -7 5 -13 6 -13 3 0 -2 -15 0 -32
|
||||
5 -18 5 -44 11 -58 14 -14 3 -36 7 -50 10 -14 3 -50 9 -80 15 -30 6 -64 12
|
||||
-75 14 -11 2 -45 6 -75 10 -30 4 -71 9 -90 12 -19 3 -53 6 -75 7 -22 1 -44 5
|
||||
-50 8 -11 7 -542 9 -548 2z m57 -404 c7 10 436 8 511 -3 22 -3 60 -8 85 -11
|
||||
25 -2 56 -6 70 -9 14 -2 43 -7 65 -10 38 -5 58 -9 115 -21 14 -3 34 -7 45 -9
|
||||
11 -2 58 -14 105 -26 47 -12 92 -23 100 -25 35 -7 279 -94 308 -109 17 -9 34
|
||||
-16 37 -16 3 1 20 -6 38 -14 17 -8 68 -31 112 -51 44 -20 82 -35 84 -35 2 1 7
|
||||
-3 10 -8 3 -5 43 -28 88 -51 45 -23 87 -48 93 -56 7 -8 17 -15 22 -15 12 0
|
||||
192 -121 196 -132 2 -4 8 -8 13 -8 10 0 119 -86 220 -172 102 -87 256 -244
|
||||
349 -357 25 -30 53 -63 63 -73 9 -10 17 -22 17 -28 0 -5 3 -10 8 -10 4 0 25
|
||||
-27 46 -60 22 -33 43 -60 48 -60 4 0 8 -5 8 -11 0 -6 11 -25 25 -43 14 -18 25
|
||||
-38 25 -44 0 -7 4 -12 8 -12 5 0 16 -15 25 -32 9 -18 30 -55 47 -83 46 -77
|
||||
161 -305 154 -305 -4 0 -2 -6 4 -12 6 -7 23 -47 40 -88 16 -41 33 -84 37 -95
|
||||
5 -11 9 -22 10 -25 0 -3 11 -36 24 -73 13 -38 21 -70 19 -73 -3 -2 -1386 -3
|
||||
-3075 -2 l-3071 3 38 110 c47 137 117 301 182 425 62 118 167 295 191 320 9
|
||||
11 17 22 17 25 0 7 39 63 58 83 6 7 26 35 44 60 18 26 37 52 43 57 6 6 34 37
|
||||
61 70 48 59 271 286 329 335 17 14 53 43 80 65 28 22 52 42 55 45 3 3 21 17
|
||||
40 30 19 14 40 28 45 32 40 32 105 78 109 78 3 0 28 16 55 35 26 19 53 35 58
|
||||
35 5 0 18 8 29 18 17 15 53 35 216 119 118 60 412 176 422 166 3 -4 6 -2 6 4
|
||||
0 6 12 13 28 16 15 3 52 12 82 21 30 9 63 19 73 21 10 2 27 7 37 10 10 3 29 8
|
||||
42 10 13 3 48 10 78 16 30 7 61 12 68 12 6 0 12 4 12 9 0 5 5 6 10 3 6 -4 34
|
||||
-2 63 4 51 11 71 13 197 26 36 4 67 9 69 11 2 2 10 -1 17 -7 8 -6 14 -7 18 0z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
@@ -15,7 +15,6 @@ export function findBuildPack(pack, packageManager = 'npm') {
|
||||
...metaData,
|
||||
...defaultBuildAndDeploy(packageManager),
|
||||
buildCommand: null,
|
||||
startCommand: null,
|
||||
publishDirectory: null,
|
||||
port: null
|
||||
};
|
||||
@@ -163,6 +162,16 @@ export function findBuildPack(pack, packageManager = 'npm') {
|
||||
port: 8000
|
||||
};
|
||||
}
|
||||
if (pack === 'laravel') {
|
||||
return {
|
||||
...metaData,
|
||||
installCommand: null,
|
||||
buildCommand: null,
|
||||
startCommand: null,
|
||||
publishDirectory: null,
|
||||
port: 80
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: 'node',
|
||||
fancyName: 'Node.js',
|
||||
@@ -188,18 +197,25 @@ export const buildPacks = [
|
||||
hoverColor: 'hover:bg-orange-700',
|
||||
color: 'bg-orange-700'
|
||||
},
|
||||
{
|
||||
name: 'docker',
|
||||
fancyName: 'Docker',
|
||||
hoverColor: 'hover:bg-sky-700',
|
||||
color: 'bg-sky-700'
|
||||
},
|
||||
|
||||
{
|
||||
name: 'php',
|
||||
fancyName: 'PHP',
|
||||
hoverColor: 'hover:bg-indigo-700',
|
||||
color: 'bg-indigo-700'
|
||||
},
|
||||
{
|
||||
name: 'laravel',
|
||||
fancyName: 'Laravel',
|
||||
hoverColor: 'hover:bg-indigo-700',
|
||||
color: 'bg-indigo-700'
|
||||
},
|
||||
{
|
||||
name: 'docker',
|
||||
fancyName: 'Docker',
|
||||
hoverColor: 'hover:bg-sky-700',
|
||||
color: 'bg-sky-700'
|
||||
},
|
||||
{
|
||||
name: 'svelte',
|
||||
fancyName: 'Svelte',
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
Application,
|
||||
ApplicationPersistentStorage
|
||||
} from '@prisma/client';
|
||||
import { setDefaultBaseImage } from '$lib/buildPacks/common';
|
||||
|
||||
export async function listApplications(teamId: string): Promise<Application[]> {
|
||||
if (teamId === '0') {
|
||||
@@ -195,8 +196,18 @@ export async function getApplication({ id, teamId }: { id: string; teamId: strin
|
||||
return s;
|
||||
});
|
||||
}
|
||||
const { baseImage, baseBuildImage, baseBuildImages, baseImages } = setDefaultBaseImage(
|
||||
body.buildPack
|
||||
);
|
||||
|
||||
return { ...body };
|
||||
// Set default build images
|
||||
if (!body.baseImage) {
|
||||
body.baseImage = baseImage;
|
||||
}
|
||||
if (!body.baseBuildImage) {
|
||||
body.baseBuildImage = baseBuildImage;
|
||||
}
|
||||
return { ...body, baseBuildImages, baseImages };
|
||||
}
|
||||
|
||||
export async function configureGitRepository({
|
||||
@@ -266,7 +277,9 @@ export async function configureApplication({
|
||||
pythonVariable,
|
||||
dockerFileLocation,
|
||||
denoMainFile,
|
||||
denoOptions
|
||||
denoOptions,
|
||||
baseImage,
|
||||
baseBuildImage
|
||||
}: {
|
||||
id: string;
|
||||
buildPack: string;
|
||||
@@ -284,6 +297,8 @@ export async function configureApplication({
|
||||
dockerFileLocation: string;
|
||||
denoMainFile: string;
|
||||
denoOptions: string;
|
||||
baseImage: string;
|
||||
baseBuildImage: string;
|
||||
}): Promise<Application> {
|
||||
return await prisma.application.update({
|
||||
where: { id },
|
||||
@@ -302,7 +317,9 @@ export async function configureApplication({
|
||||
pythonVariable,
|
||||
dockerFileLocation,
|
||||
denoMainFile,
|
||||
denoOptions
|
||||
denoOptions,
|
||||
baseImage,
|
||||
baseBuildImage
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,11 +11,12 @@ import generator from 'generate-password';
|
||||
import forge from 'node-forge';
|
||||
import getPort, { portNumbers } from 'get-port';
|
||||
|
||||
export function generatePassword(length = 24): string {
|
||||
export function generatePassword(length = 24, symbols = false): string {
|
||||
return generator.generate({
|
||||
length,
|
||||
numbers: true,
|
||||
strict: true
|
||||
strict: true,
|
||||
symbols
|
||||
});
|
||||
}
|
||||
|
||||
@@ -154,6 +155,7 @@ export function generateDatabaseConfiguration(database: Database & { settings: D
|
||||
ulimits: Record<string, unknown>;
|
||||
privatePort: number;
|
||||
environmentVariables: {
|
||||
POSTGRESQL_POSTGRES_PASSWORD: string;
|
||||
POSTGRESQL_USERNAME: string;
|
||||
POSTGRESQL_PASSWORD: string;
|
||||
POSTGRESQL_DATABASE: string;
|
||||
@@ -220,6 +222,7 @@ export function generateDatabaseConfiguration(database: Database & { settings: D
|
||||
return {
|
||||
privatePort: 5432,
|
||||
environmentVariables: {
|
||||
POSTGRESQL_POSTGRES_PASSWORD: rootUserPassword,
|
||||
POSTGRESQL_PASSWORD: dbUserPassword,
|
||||
POSTGRESQL_USERNAME: dbUser,
|
||||
POSTGRESQL_DATABASE: defaultDatabase
|
||||
|
||||
@@ -1,9 +1,29 @@
|
||||
import { decrypt, encrypt } from '$lib/crypto';
|
||||
import type { Minio, Service } from '@prisma/client';
|
||||
import type { Minio, Prisma, Service } from '@prisma/client';
|
||||
import cuid from 'cuid';
|
||||
import { generatePassword } from '.';
|
||||
import { prisma } from './common';
|
||||
|
||||
const include: Prisma.ServiceInclude = {
|
||||
destinationDocker: true,
|
||||
persistentStorage: true,
|
||||
serviceSecret: true,
|
||||
minio: true,
|
||||
plausibleAnalytics: true,
|
||||
vscodeserver: true,
|
||||
wordpress: true,
|
||||
ghost: true,
|
||||
meiliSearch: true,
|
||||
umami: true,
|
||||
hasura: true,
|
||||
fider: true
|
||||
};
|
||||
export async function listServicesWithIncludes() {
|
||||
return await prisma.service.findMany({
|
||||
include,
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
}
|
||||
export async function listServices(teamId: string): Promise<Service[]> {
|
||||
if (teamId === '0') {
|
||||
return await prisma.service.findMany({ include: { teams: true } });
|
||||
@@ -30,35 +50,21 @@ export async function getService({ id, teamId }: { id: string; teamId: string })
|
||||
if (teamId === '0') {
|
||||
body = await prisma.service.findFirst({
|
||||
where: { id },
|
||||
include: {
|
||||
destinationDocker: true,
|
||||
plausibleAnalytics: true,
|
||||
minio: true,
|
||||
vscodeserver: true,
|
||||
wordpress: true,
|
||||
ghost: true,
|
||||
serviceSecret: true,
|
||||
meiliSearch: true,
|
||||
persistentStorage: true
|
||||
}
|
||||
include
|
||||
});
|
||||
} else {
|
||||
body = await prisma.service.findFirst({
|
||||
where: { id, teams: { some: { id: teamId } } },
|
||||
include: {
|
||||
destinationDocker: true,
|
||||
plausibleAnalytics: true,
|
||||
minio: true,
|
||||
vscodeserver: true,
|
||||
wordpress: true,
|
||||
ghost: true,
|
||||
serviceSecret: true,
|
||||
meiliSearch: true,
|
||||
persistentStorage: true
|
||||
}
|
||||
include
|
||||
});
|
||||
}
|
||||
|
||||
if (body?.serviceSecret.length > 0) {
|
||||
body.serviceSecret = body.serviceSecret.map((s) => {
|
||||
s.value = decrypt(s.value);
|
||||
return s;
|
||||
});
|
||||
}
|
||||
if (body.plausibleAnalytics?.postgresqlPassword)
|
||||
body.plausibleAnalytics.postgresqlPassword = decrypt(
|
||||
body.plausibleAnalytics.postgresqlPassword
|
||||
@@ -85,15 +91,25 @@ export async function getService({ id, teamId }: { id: string; teamId: string })
|
||||
|
||||
if (body.meiliSearch?.masterKey) body.meiliSearch.masterKey = decrypt(body.meiliSearch.masterKey);
|
||||
|
||||
if (body?.serviceSecret.length > 0) {
|
||||
body.serviceSecret = body.serviceSecret.map((s) => {
|
||||
s.value = decrypt(s.value);
|
||||
return s;
|
||||
});
|
||||
}
|
||||
if (body.wordpress?.ftpPassword) {
|
||||
body.wordpress.ftpPassword = decrypt(body.wordpress.ftpPassword);
|
||||
}
|
||||
if (body.wordpress?.ftpPassword) body.wordpress.ftpPassword = decrypt(body.wordpress.ftpPassword);
|
||||
|
||||
if (body.umami?.postgresqlPassword)
|
||||
body.umami.postgresqlPassword = decrypt(body.umami.postgresqlPassword);
|
||||
if (body.umami?.umamiAdminPassword)
|
||||
body.umami.umamiAdminPassword = decrypt(body.umami.umamiAdminPassword);
|
||||
if (body.umami?.hashSalt) body.umami.hashSalt = decrypt(body.umami.hashSalt);
|
||||
|
||||
if (body.hasura?.postgresqlPassword)
|
||||
body.hasura.postgresqlPassword = decrypt(body.hasura.postgresqlPassword);
|
||||
if (body.hasura?.graphQLAdminPassword)
|
||||
body.hasura.graphQLAdminPassword = decrypt(body.hasura.graphQLAdminPassword);
|
||||
|
||||
if (body.fider?.postgresqlPassword)
|
||||
body.fider.postgresqlPassword = decrypt(body.fider.postgresqlPassword);
|
||||
if (body.fider?.jwtSecret) body.fider.jwtSecret = decrypt(body.fider.jwtSecret);
|
||||
if (body.fider?.emailSmtpPassword)
|
||||
body.fider.emailSmtpPassword = decrypt(body.fider.emailSmtpPassword);
|
||||
|
||||
const settings = await prisma.setting.findFirst();
|
||||
|
||||
return { ...body, settings };
|
||||
@@ -219,6 +235,65 @@ export async function configureServiceType({
|
||||
meiliSearch: { create: { masterKey } }
|
||||
}
|
||||
});
|
||||
} else if (type === 'umami') {
|
||||
const umamiAdminPassword = encrypt(generatePassword());
|
||||
const postgresqlUser = cuid();
|
||||
const postgresqlPassword = encrypt(generatePassword());
|
||||
const postgresqlDatabase = 'umami';
|
||||
const hashSalt = encrypt(generatePassword(64));
|
||||
await prisma.service.update({
|
||||
where: { id },
|
||||
data: {
|
||||
type,
|
||||
umami: {
|
||||
create: {
|
||||
umamiAdminPassword,
|
||||
postgresqlDatabase,
|
||||
postgresqlPassword,
|
||||
postgresqlUser,
|
||||
hashSalt
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (type === 'hasura') {
|
||||
const postgresqlUser = cuid();
|
||||
const postgresqlPassword = encrypt(generatePassword());
|
||||
const postgresqlDatabase = 'hasura';
|
||||
const graphQLAdminPassword = encrypt(generatePassword());
|
||||
await prisma.service.update({
|
||||
where: { id },
|
||||
data: {
|
||||
type,
|
||||
hasura: {
|
||||
create: {
|
||||
postgresqlDatabase,
|
||||
postgresqlPassword,
|
||||
postgresqlUser,
|
||||
graphQLAdminPassword
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (type === 'fider') {
|
||||
const postgresqlUser = cuid();
|
||||
const postgresqlPassword = encrypt(generatePassword());
|
||||
const postgresqlDatabase = 'fider';
|
||||
const jwtSecret = encrypt(generatePassword(64, true));
|
||||
await prisma.service.update({
|
||||
where: { id },
|
||||
data: {
|
||||
type,
|
||||
fider: {
|
||||
create: {
|
||||
postgresqlDatabase,
|
||||
postgresqlPassword,
|
||||
postgresqlUser,
|
||||
jwtSecret
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,52 +352,53 @@ export async function updateService({
|
||||
return await prisma.service.update({ where: { id }, data: { fqdn, name } });
|
||||
}
|
||||
|
||||
export async function updateLanguageToolService({
|
||||
export async function updateFiderService({
|
||||
id,
|
||||
fqdn,
|
||||
name
|
||||
name,
|
||||
emailNoreply,
|
||||
emailMailgunApiKey,
|
||||
emailMailgunDomain,
|
||||
emailMailgunRegion,
|
||||
emailSmtpHost,
|
||||
emailSmtpPort,
|
||||
emailSmtpUser,
|
||||
emailSmtpPassword,
|
||||
emailSmtpEnableStartTls
|
||||
}: {
|
||||
id: string;
|
||||
fqdn: string;
|
||||
name: string;
|
||||
emailNoreply: string;
|
||||
emailMailgunApiKey: string;
|
||||
emailMailgunDomain: string;
|
||||
emailMailgunRegion: string;
|
||||
emailSmtpHost: string;
|
||||
emailSmtpPort: number;
|
||||
emailSmtpUser: string;
|
||||
emailSmtpPassword: string;
|
||||
emailSmtpEnableStartTls: boolean;
|
||||
}): Promise<Service> {
|
||||
return await prisma.service.update({ where: { id }, data: { fqdn, name } });
|
||||
}
|
||||
|
||||
export async function updateMeiliSearchService({
|
||||
id,
|
||||
fqdn,
|
||||
name
|
||||
}: {
|
||||
id: string;
|
||||
fqdn: string;
|
||||
name: string;
|
||||
}): Promise<Service> {
|
||||
return await prisma.service.update({ where: { id }, data: { fqdn, name } });
|
||||
}
|
||||
|
||||
export async function updateVaultWardenService({
|
||||
id,
|
||||
fqdn,
|
||||
name
|
||||
}: {
|
||||
id: string;
|
||||
fqdn: string;
|
||||
name: string;
|
||||
}): Promise<Service> {
|
||||
return await prisma.service.update({ where: { id }, data: { fqdn, name } });
|
||||
}
|
||||
|
||||
export async function updateVsCodeServer({
|
||||
id,
|
||||
fqdn,
|
||||
name
|
||||
}: {
|
||||
id: string;
|
||||
fqdn: string;
|
||||
name: string;
|
||||
}): Promise<Service> {
|
||||
return await prisma.service.update({ where: { id }, data: { fqdn, name } });
|
||||
return await prisma.service.update({
|
||||
where: { id },
|
||||
data: {
|
||||
fqdn,
|
||||
name,
|
||||
fider: {
|
||||
update: {
|
||||
emailNoreply,
|
||||
emailMailgunApiKey,
|
||||
emailMailgunDomain,
|
||||
emailMailgunRegion,
|
||||
emailSmtpHost,
|
||||
emailSmtpPort,
|
||||
emailSmtpUser,
|
||||
emailSmtpPassword,
|
||||
emailSmtpEnableStartTls
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateWordpress({
|
||||
@@ -374,7 +450,10 @@ export async function updateGhostService({
|
||||
export async function removeService({ id }: { id: string }): Promise<void> {
|
||||
await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id } });
|
||||
await prisma.meiliSearch.deleteMany({ where: { serviceId: id } });
|
||||
await prisma.fider.deleteMany({ where: { serviceId: id } });
|
||||
await prisma.ghost.deleteMany({ where: { serviceId: id } });
|
||||
await prisma.umami.deleteMany({ where: { serviceId: id } });
|
||||
await prisma.hasura.deleteMany({ where: { serviceId: id } });
|
||||
await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } });
|
||||
await prisma.minio.deleteMany({ where: { serviceId: id } });
|
||||
await prisma.vscodeserver.deleteMany({ where: { serviceId: id } });
|
||||
|
||||
@@ -3,6 +3,34 @@ import { promises as fs } from 'fs';
|
||||
import { checkPnpm } from './buildPacks/common';
|
||||
import { saveBuildLog } from './common';
|
||||
|
||||
export async function buildCacheImageForLaravel(data, imageForBuild) {
|
||||
const { applicationId, tag, workdir, docker, buildId, debug, secrets, pullmergeRequestId } = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
Dockerfile.push(`FROM ${imageForBuild}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
if (secrets.length > 0) {
|
||||
secrets.forEach((secret) => {
|
||||
if (secret.isBuildSecret) {
|
||||
if (pullmergeRequestId) {
|
||||
if (secret.isPRMRSecret) {
|
||||
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
||||
}
|
||||
} else {
|
||||
if (!secret.isPRMRSecret) {
|
||||
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Dockerfile.push(`COPY *.json *.mix.js /app/`);
|
||||
Dockerfile.push(`COPY resources /app/resources`);
|
||||
Dockerfile.push(`RUN yarn install && yarn production`);
|
||||
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
|
||||
await buildImage({ applicationId, tag, workdir, docker, buildId, isCache: true, debug });
|
||||
}
|
||||
|
||||
export async function buildCacheImageWithNode(data, imageForBuild) {
|
||||
const {
|
||||
applicationId,
|
||||
@@ -21,7 +49,7 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
|
||||
const Dockerfile: Array<string> = [];
|
||||
Dockerfile.push(`FROM ${imageForBuild}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`LABEL coolify.image=true`);
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
if (secrets.length > 0) {
|
||||
secrets.forEach((secret) => {
|
||||
if (secret.isBuildSecret) {
|
||||
@@ -41,10 +69,11 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
|
||||
Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm');
|
||||
Dockerfile.push('RUN pnpm add -g pnpm');
|
||||
}
|
||||
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
|
||||
if (installCommand) {
|
||||
Dockerfile.push(`COPY .${baseDirectory || ''}/package.json ./`);
|
||||
Dockerfile.push(`RUN ${installCommand}`);
|
||||
}
|
||||
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
|
||||
Dockerfile.push(`RUN ${buildCommand}`);
|
||||
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
|
||||
await buildImage({ applicationId, tag, workdir, docker, buildId, isCache: true, debug });
|
||||
@@ -65,11 +94,13 @@ export async function buildCacheImageWithCargo(data, imageForBuild) {
|
||||
} = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
Dockerfile.push(`FROM ${imageForBuild} as planner-${applicationId}`);
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push('RUN cargo install cargo-chef');
|
||||
Dockerfile.push('COPY . .');
|
||||
Dockerfile.push('RUN cargo chef prepare --recipe-path recipe.json');
|
||||
Dockerfile.push(`FROM ${imageForBuild}`);
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push('RUN cargo install cargo-chef');
|
||||
Dockerfile.push(`COPY --from=planner-${applicationId} /app/recipe.json recipe.json`);
|
||||
|
||||
@@ -6,6 +6,7 @@ import crypto from 'crypto';
|
||||
import { checkContainer, checkHAProxy } from '.';
|
||||
import { asyncExecShell, getDomain, getEngine } from '$lib/common';
|
||||
import { supportedServiceTypesAndVersions } from '$lib/components/common';
|
||||
import { listServicesWithIncludes } from '$lib/database';
|
||||
|
||||
const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555';
|
||||
|
||||
@@ -94,6 +95,8 @@ backend {{domain}}
|
||||
{{/isHttps}}
|
||||
http-request add-header X-Forwarded-Host %[req.hdr(host),lower]
|
||||
server {{id}} {{id}}:{{port}}
|
||||
compression algo gzip
|
||||
compression type text/html text/css text/plain text/xml text/x-component text/javascript application/x-javascript application/javascript application/json application/manifest+json application/vnd.api+json application/xml application/xhtml+xml application/rss+xml application/atom+xml application/vnd.ms-fontobject application/x-font-ttf application/x-font-opentype application/x-font-truetype image/svg+xml image/x-icon image/vnd.microsoft.icon font/ttf font/eot font/otf font/opentype
|
||||
{{/isRunning}}
|
||||
{{/applications}}
|
||||
|
||||
@@ -110,6 +113,8 @@ backend {{domain}}
|
||||
{{/isHttps}}
|
||||
http-request add-header X-Forwarded-Host %[req.hdr(host),lower]
|
||||
server {{id}} {{id}}:{{port}}
|
||||
compression algo gzip
|
||||
compression type text/html text/css text/plain text/xml text/x-component text/javascript application/x-javascript application/javascript application/json application/manifest+json application/vnd.api+json application/xml application/xhtml+xml application/rss+xml application/atom+xml application/vnd.ms-fontobject application/x-font-ttf application/x-font-opentype application/x-font-truetype image/svg+xml image/x-icon image/vnd.microsoft.icon font/ttf font/eot font/otf font/opentype
|
||||
{{/isRunning}}
|
||||
{{/services}}
|
||||
|
||||
@@ -125,6 +130,8 @@ backend {{domain}}
|
||||
{{/isHttps}}
|
||||
http-request add-header X-Forwarded-Host %[req.hdr(host),lower]
|
||||
server {{id}} {{id}}:{{port}} check fall 10
|
||||
compression algo gzip
|
||||
compression type text/html text/css text/plain text/xml text/x-component text/javascript application/x-javascript application/javascript application/json application/manifest+json application/vnd.api+json application/xml application/xhtml+xml application/rss+xml application/atom+xml application/vnd.ms-fontobject application/x-font-ttf application/x-font-opentype application/x-font-truetype image/svg+xml image/x-icon image/vnd.microsoft.icon font/ttf font/eot font/otf font/opentype
|
||||
{{/coolify}}
|
||||
`;
|
||||
|
||||
@@ -208,17 +215,7 @@ export async function configureHAProxy(): Promise<void> {
|
||||
}
|
||||
}
|
||||
}
|
||||
const services = await db.prisma.service.findMany({
|
||||
include: {
|
||||
destinationDocker: true,
|
||||
minio: true,
|
||||
plausibleAnalytics: true,
|
||||
vscodeserver: true,
|
||||
wordpress: true,
|
||||
ghost: true,
|
||||
meiliSearch: true
|
||||
}
|
||||
});
|
||||
const services = await listServicesWithIncludes();
|
||||
|
||||
for (const service of services) {
|
||||
const { fqdn, id, type, destinationDocker, destinationDockerId, updatedAt } = service;
|
||||
|
||||
@@ -7,6 +7,7 @@ import fs from 'fs/promises';
|
||||
import getPort, { portNumbers } from 'get-port';
|
||||
import { supportedServiceTypesAndVersions } from '$lib/components/common';
|
||||
import { promises as dns } from 'dns';
|
||||
import { listServicesWithIncludes } from '$lib/database';
|
||||
|
||||
export async function letsEncrypt(domain: string, id?: string, isCoolify = false): Promise<void> {
|
||||
try {
|
||||
@@ -108,6 +109,7 @@ export async function generateSSLCerts(): Promise<void> {
|
||||
include: { destinationDocker: true, settings: true },
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
const { fqdn, isDNSCheckEnabled } = await db.prisma.setting.findFirst();
|
||||
for (const application of applications) {
|
||||
try {
|
||||
if (application.fqdn && application.destinationDockerId) {
|
||||
@@ -145,19 +147,7 @@ export async function generateSSLCerts(): Promise<void> {
|
||||
console.log(`Error during generateSSLCerts with ${application.fqdn}: ${error}`);
|
||||
}
|
||||
}
|
||||
const services = await db.prisma.service.findMany({
|
||||
include: {
|
||||
destinationDocker: true,
|
||||
minio: true,
|
||||
plausibleAnalytics: true,
|
||||
vscodeserver: true,
|
||||
wordpress: true,
|
||||
ghost: true,
|
||||
meiliSearch: true
|
||||
},
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
|
||||
const services = await listServicesWithIncludes();
|
||||
for (const service of services) {
|
||||
try {
|
||||
if (service.fqdn && service.destinationDockerId) {
|
||||
@@ -181,7 +171,6 @@ export async function generateSSLCerts(): Promise<void> {
|
||||
console.log(`Error during generateSSLCerts with ${service.fqdn}: ${error}`);
|
||||
}
|
||||
}
|
||||
const { fqdn } = await db.prisma.setting.findFirst();
|
||||
if (fqdn) {
|
||||
const domain = getDomain(fqdn);
|
||||
const isHttps = fqdn.startsWith('https://');
|
||||
@@ -203,73 +192,99 @@ export async function generateSSLCerts(): Promise<void> {
|
||||
file.endsWith('.pem') && certificates.push(file.replace(/\.pem$/, ''));
|
||||
}
|
||||
}
|
||||
const resolver = new dns.Resolver({ timeout: 2000 });
|
||||
resolver.setServers(['8.8.8.8', '1.1.1.1']);
|
||||
let ipv4, ipv6;
|
||||
try {
|
||||
ipv4 = await (await asyncExecShell(`curl -4s https://ifconfig.io`)).stdout;
|
||||
} catch (error) {}
|
||||
try {
|
||||
ipv6 = await (await asyncExecShell(`curl -6s https://ifconfig.io`)).stdout;
|
||||
} catch (error) {}
|
||||
for (const ssl of ssls) {
|
||||
if (!dev) {
|
||||
if (
|
||||
certificates.includes(ssl.domain) ||
|
||||
certificates.includes(ssl.domain.replace('www.', ''))
|
||||
) {
|
||||
// console.log(`Certificate for ${ssl.domain} already exists`);
|
||||
} else {
|
||||
// Checking DNS entry before generating certificate
|
||||
if (ipv4 || ipv6) {
|
||||
let domains4 = [];
|
||||
let domains6 = [];
|
||||
try {
|
||||
domains4 = await resolver.resolve4(ssl.domain);
|
||||
} catch (error) {}
|
||||
try {
|
||||
domains6 = await resolver.resolve6(ssl.domain);
|
||||
} catch (error) {}
|
||||
if (domains4.length > 0 || domains6.length > 0) {
|
||||
if (
|
||||
(ipv4 && domains4.includes(ipv4.replace('\n', ''))) ||
|
||||
(ipv6 && domains6.includes(ipv6.replace('\n', '')))
|
||||
) {
|
||||
console.log('Generating SSL for', ssl.domain);
|
||||
return await letsEncrypt(ssl.domain, ssl.id, ssl.isCoolify);
|
||||
if (isDNSCheckEnabled) {
|
||||
const resolver = new dns.Resolver({ timeout: 2000 });
|
||||
resolver.setServers(['8.8.8.8', '1.1.1.1']);
|
||||
let ipv4, ipv6;
|
||||
try {
|
||||
ipv4 = await (await asyncExecShell(`curl -4s https://ifconfig.io`)).stdout;
|
||||
} catch (error) {}
|
||||
try {
|
||||
ipv6 = await (await asyncExecShell(`curl -6s https://ifconfig.io`)).stdout;
|
||||
} catch (error) {}
|
||||
for (const ssl of ssls) {
|
||||
if (!dev) {
|
||||
if (
|
||||
certificates.includes(ssl.domain) ||
|
||||
certificates.includes(ssl.domain.replace('www.', ''))
|
||||
) {
|
||||
// console.log(`Certificate for ${ssl.domain} already exists`);
|
||||
} else {
|
||||
// Checking DNS entry before generating certificate
|
||||
if (ipv4 || ipv6) {
|
||||
let domains4 = [];
|
||||
let domains6 = [];
|
||||
try {
|
||||
domains4 = await resolver.resolve4(ssl.domain);
|
||||
} catch (error) {}
|
||||
try {
|
||||
domains6 = await resolver.resolve6(ssl.domain);
|
||||
} catch (error) {}
|
||||
if (domains4.length > 0 || domains6.length > 0) {
|
||||
if (
|
||||
(ipv4 && domains4.includes(ipv4.replace('\n', ''))) ||
|
||||
(ipv6 && domains6.includes(ipv6.replace('\n', '')))
|
||||
) {
|
||||
console.log('Generating SSL for', ssl.domain);
|
||||
return await letsEncrypt(ssl.domain, ssl.id, ssl.isCoolify);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('DNS settings is incorrect for', ssl.domain, 'skipping.');
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
certificates.includes(ssl.domain) ||
|
||||
certificates.includes(ssl.domain.replace('www.', ''))
|
||||
) {
|
||||
console.log(`Certificate for ${ssl.domain} already exists`);
|
||||
} else {
|
||||
// Checking DNS entry before generating certificate
|
||||
if (ipv4 || ipv6) {
|
||||
let domains4 = [];
|
||||
let domains6 = [];
|
||||
try {
|
||||
domains4 = await resolver.resolve4(ssl.domain);
|
||||
} catch (error) {}
|
||||
try {
|
||||
domains6 = await resolver.resolve6(ssl.domain);
|
||||
} catch (error) {}
|
||||
if (domains4.length > 0 || domains6.length > 0) {
|
||||
if (
|
||||
(ipv4 && domains4.includes(ipv4.replace('\n', ''))) ||
|
||||
(ipv6 && domains6.includes(ipv6.replace('\n', '')))
|
||||
) {
|
||||
console.log('Generating SSL for', ssl.domain);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('DNS settings is incorrect for', ssl.domain, 'skipping.');
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!dev) {
|
||||
for (const ssl of ssls) {
|
||||
if (
|
||||
certificates.includes(ssl.domain) ||
|
||||
certificates.includes(ssl.domain.replace('www.', ''))
|
||||
) {
|
||||
} else {
|
||||
console.log('Generating SSL for', ssl.domain);
|
||||
return await letsEncrypt(ssl.domain, ssl.id, ssl.isCoolify);
|
||||
}
|
||||
console.log('DNS settings is incorrect for', ssl.domain, 'skipping.');
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
certificates.includes(ssl.domain) ||
|
||||
certificates.includes(ssl.domain.replace('www.', ''))
|
||||
) {
|
||||
console.log(`Certificate for ${ssl.domain} already exists`);
|
||||
} else {
|
||||
// Checking DNS entry before generating certificate
|
||||
if (ipv4 || ipv6) {
|
||||
let domains4 = [];
|
||||
let domains6 = [];
|
||||
try {
|
||||
domains4 = await resolver.resolve4(ssl.domain);
|
||||
} catch (error) {}
|
||||
try {
|
||||
domains6 = await resolver.resolve6(ssl.domain);
|
||||
} catch (error) {}
|
||||
if (domains4.length > 0 || domains6.length > 0) {
|
||||
if (
|
||||
(ipv4 && domains4.includes(ipv4.replace('\n', ''))) ||
|
||||
(ipv6 && domains6.includes(ipv6.replace('\n', '')))
|
||||
) {
|
||||
console.log('Generating SSL for', ssl.domain);
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (const ssl of ssls) {
|
||||
if (
|
||||
certificates.includes(ssl.domain) ||
|
||||
certificates.includes(ssl.domain.replace('www.', ''))
|
||||
) {
|
||||
console.log(`Certificate for ${ssl.domain} already exists`);
|
||||
} else {
|
||||
console.log('Generating SSL for', ssl.domain);
|
||||
}
|
||||
console.log('DNS settings is incorrect for', ssl.domain, 'skipping.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"no_branches_found": "No branches found",
|
||||
"configure_build_pack": "Configure Build Pack",
|
||||
"scanning_repository_suggest_build_pack": "Scanning repository to suggest a build pack for you...",
|
||||
"found_lock_file": "Found lock file for <span class=\"font-bold text-orange-500 pl-1\">{{packageManager}}</span>. Using it for predefined commands commands.",
|
||||
"found_lock_file": "Found lock file for <span class=\"font-bold text-orange-500 px-1\"> {{packageManager}}</span>.Using it for predefined commands commands.",
|
||||
"configure_destination": "Configure Destination",
|
||||
"no_configurable_destination": "No configurable Destination found",
|
||||
"select_a_repository_project": "Select a Repository / Project",
|
||||
@@ -184,6 +184,10 @@
|
||||
"git_source": "Git Source",
|
||||
"git_repository": "Git Repository",
|
||||
"build_pack": "Build Pack",
|
||||
"base_image": "Deplyoment Image",
|
||||
"base_image_explainer": "Image that will be used for the deployment.",
|
||||
"base_build_image": "Build Image",
|
||||
"base_build_image_explainer": "Image that will be used during the build process.",
|
||||
"destination": "Destination",
|
||||
"application": "Application",
|
||||
"url_fqdn": "URL (FQDN)",
|
||||
@@ -226,7 +230,8 @@
|
||||
"permission_denied_start_database": "You do not have permission to start the database.",
|
||||
"delete_database": "Delete Database",
|
||||
"permission_denied_delete_database": "You do not have permission to delete a Database",
|
||||
"no_databases_found": "No databases found"
|
||||
"no_databases_found": "No databases found",
|
||||
"logs": "Database Logs"
|
||||
},
|
||||
"destination": {
|
||||
"delete_destination": "Delete Destination",
|
||||
@@ -291,18 +296,24 @@
|
||||
"permission_denied_start_service": "You do not have permission to start the service.",
|
||||
"delete_service": "Delete Service",
|
||||
"permission_denied_delete_service": "You do not have permission to delete a service.",
|
||||
"no_service": "No services found"
|
||||
"no_service": "No services found",
|
||||
"logs": "Service Logs"
|
||||
},
|
||||
"setting": {
|
||||
"change_language": "Change Language",
|
||||
"permission_denied": "You do not have permission to do this. \\nAsk an admin to modify your permissions.",
|
||||
"domain_removed": "Domain removed",
|
||||
"ssl_explainer": "If you specify <span class='text-yellow-500 font-bold'>https</span>, Coolify will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-yellow-500 font-bold'>www</span>, Coolify will be redirected (302) from non-www and vice versa.",
|
||||
"ssl_explainer": "If you specify <span class='text-yellow-500 font-bold'>https</span>, Coolify will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-yellow-500 font-bold'>www</span>, Coolify will be redirected (302) from non-www and vice versa.<br><br><span class='text-yellow-500 font-bold'>WARNING:</span> If you change an already set domain, it will brake webhooks and other integrations! You need to manually update them.",
|
||||
"must_remove_domain_before_changing": "Must remove the domain before you can change this setting.",
|
||||
"registration_allowed": "Registration allowed?",
|
||||
"registration_allowed_explainer": "Allow further registrations to the application. <br>It's turned off after the first registration.",
|
||||
"coolify_proxy_settings": "Coolify Proxy Settings",
|
||||
"credential_stat_explainer": "Credentials for <a class=\"text-white font-bold\" href=\"{{link}}\" target=\"_blank\">stats</a> page."
|
||||
"credential_stat_explainer": "Credentials for <a class=\"text-white font-bold\" href=\"{{link}}\" target=\"_blank\">stats</a> page.",
|
||||
"auto_update_enabled": "Auto update enabled?",
|
||||
"auto_update_enabled_explainer": "Enable automatic updates for Coolify. It will be done automatically behind the scenes, if there is no build process running.",
|
||||
"generate_www_non_www_ssl": "It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-yellow-500'>both DNS entries</span> set in advance.<br><br>Service needs to be restarted.",
|
||||
"is_dns_check_enabled": "DNS check enabled?",
|
||||
"is_dns_check_enabled_explainer": "You can disable DNS check before creating SSL certificates.<br><br>Turning it off is useful when Coolify is behind a reverse proxy or tunnel."
|
||||
},
|
||||
"team": {
|
||||
"pending_invitations": "Pending invitations",
|
||||
|
||||
42
src/lib/queues/autoUpdater.ts
Normal file
42
src/lib/queues/autoUpdater.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { prisma } from '$lib/database';
|
||||
import { buildQueue } from '.';
|
||||
import got from 'got';
|
||||
import { asyncExecShell, version } from '$lib/common';
|
||||
import compare from 'compare-versions';
|
||||
import { dev } from '$app/env';
|
||||
|
||||
export default async function (): Promise<void> {
|
||||
try {
|
||||
const currentVersion = version;
|
||||
const { isAutoUpdateEnabled } = await prisma.setting.findFirst();
|
||||
if (isAutoUpdateEnabled) {
|
||||
const versions = await got
|
||||
.get(
|
||||
`https://get.coollabs.io/versions.json?appId=${process.env['COOLIFY_APP_ID']}&version=${currentVersion}`
|
||||
)
|
||||
.json();
|
||||
const latestVersion = versions['coolify'].main.version;
|
||||
const isUpdateAvailable = compare(latestVersion, currentVersion);
|
||||
if (isUpdateAvailable === 1) {
|
||||
const activeCount = await buildQueue.getActiveCount();
|
||||
if (activeCount === 0) {
|
||||
if (!dev) {
|
||||
await buildQueue.pause();
|
||||
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 coolify-redis && docker rm coolify coolify-redis && docker compose up -d --force-recreate"`
|
||||
);
|
||||
} else {
|
||||
await buildQueue.pause();
|
||||
console.log('Updating (not really in dev mode).');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
await buildQueue.resume();
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,9 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
|
||||
pythonWSGI,
|
||||
pythonModule,
|
||||
pythonVariable,
|
||||
denoOptions
|
||||
denoOptions,
|
||||
baseImage,
|
||||
baseBuildImage
|
||||
} = job.data;
|
||||
let {
|
||||
branch,
|
||||
@@ -186,7 +188,7 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
|
||||
//
|
||||
}
|
||||
if (!imageFound || deployNeeded) {
|
||||
await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId);
|
||||
await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId, baseImage);
|
||||
if (buildpacks[buildPack])
|
||||
await buildpacks[buildPack]({
|
||||
buildId,
|
||||
@@ -217,7 +219,9 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
|
||||
pythonVariable,
|
||||
dockerFileLocation,
|
||||
denoMainFile,
|
||||
denoOptions
|
||||
denoOptions,
|
||||
baseImage,
|
||||
baseBuildImage
|
||||
});
|
||||
else {
|
||||
await saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId });
|
||||
|
||||
@@ -4,34 +4,73 @@ export default async function (): Promise<void> {
|
||||
const destinationDockers = await prisma.destinationDocker.findMany();
|
||||
const engines = [...new Set(destinationDockers.map(({ engine }) => engine))];
|
||||
for (const engine of engines) {
|
||||
let lowDiskSpace = false;
|
||||
const host = getEngine(engine);
|
||||
// Cleanup old coolify images
|
||||
try {
|
||||
let { stdout: images } = await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker images coollabsio/coolify --filter before="coollabsio/coolify:${version}" -q | xargs `
|
||||
const { stdout } = await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker exec coolify sh -c 'df -kPT /'`
|
||||
);
|
||||
images = images.trim();
|
||||
if (images) {
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker rmi -f ${images}`);
|
||||
let lines = stdout.trim().split('\n');
|
||||
let header = lines[0];
|
||||
let regex =
|
||||
/^Filesystem\s+|Type\s+|1024-blocks|\s+Used|\s+Available|\s+Capacity|\s+Mounted on\s*$/g;
|
||||
const boundaries = [];
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(header))) {
|
||||
boundaries.push(match[0].length);
|
||||
}
|
||||
|
||||
boundaries[boundaries.length - 1] = -1;
|
||||
const data = lines.slice(1).map((line) => {
|
||||
const cl = boundaries.map((boundary) => {
|
||||
const column = boundary > 0 ? line.slice(0, boundary) : line;
|
||||
line = line.slice(boundary);
|
||||
return column.trim();
|
||||
});
|
||||
return {
|
||||
capacity: Number.parseInt(cl[5], 10) / 100
|
||||
};
|
||||
});
|
||||
if (data.length > 0) {
|
||||
const { capacity } = data[0];
|
||||
if (capacity > 0.8) {
|
||||
lowDiskSpace = true;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
//console.log(error);
|
||||
console.log(error);
|
||||
}
|
||||
try {
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker container prune -f`);
|
||||
} catch (error) {
|
||||
//console.log(error);
|
||||
}
|
||||
try {
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker image prune -f --filter "until=2h"`);
|
||||
} catch (error) {
|
||||
//console.log(error);
|
||||
}
|
||||
// Cleanup old images older than a day
|
||||
try {
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker image prune --filter "until=72h" -a -f`);
|
||||
} catch (error) {
|
||||
//console.log(error);
|
||||
console.log(`Is LowDiskSpace detected? ${lowDiskSpace}`);
|
||||
if (lowDiskSpace) {
|
||||
// Cleanup old coolify images
|
||||
try {
|
||||
let { stdout: images } = await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker images coollabsio/coolify --filter before="coollabsio/coolify:${version}" -q | xargs `
|
||||
);
|
||||
images = images.trim();
|
||||
if (images) {
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker rmi -f ${images}`);
|
||||
}
|
||||
} catch (error) {
|
||||
//console.log(error);
|
||||
}
|
||||
try {
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker container prune -f`);
|
||||
} catch (error) {
|
||||
//console.log(error);
|
||||
}
|
||||
try {
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker image prune -f --filter "until=2h"`);
|
||||
} catch (error) {
|
||||
//console.log(error);
|
||||
}
|
||||
// Cleanup old images older than a day
|
||||
try {
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker image prune --filter "until=72h" -a -f`);
|
||||
} catch (error) {
|
||||
//console.log(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import proxy from './proxy';
|
||||
import proxyTcpHttp from './proxyTcpHttp';
|
||||
import ssl from './ssl';
|
||||
import sslrenewal from './sslrenewal';
|
||||
import autoUpdater from './autoUpdater';
|
||||
|
||||
import { asyncExecShell, saveBuildLog } from '$lib/common';
|
||||
|
||||
@@ -34,19 +35,22 @@ const cron = async (): Promise<void> => {
|
||||
new QueueScheduler('cleanup', connectionOptions);
|
||||
new QueueScheduler('ssl', connectionOptions);
|
||||
new QueueScheduler('sslRenew', connectionOptions);
|
||||
new QueueScheduler('autoUpdater', connectionOptions);
|
||||
|
||||
const queue = {
|
||||
proxy: new Queue('proxy', { ...connectionOptions }),
|
||||
proxyTcpHttp: new Queue('proxyTcpHttp', { ...connectionOptions }),
|
||||
cleanup: new Queue('cleanup', { ...connectionOptions }),
|
||||
ssl: new Queue('ssl', { ...connectionOptions }),
|
||||
sslRenew: new Queue('sslRenew', { ...connectionOptions })
|
||||
sslRenew: new Queue('sslRenew', { ...connectionOptions }),
|
||||
autoUpdater: new Queue('autoUpdater', { ...connectionOptions })
|
||||
};
|
||||
await queue.proxy.drain();
|
||||
await queue.proxyTcpHttp.drain();
|
||||
await queue.cleanup.drain();
|
||||
await queue.ssl.drain();
|
||||
await queue.sslRenew.drain();
|
||||
await queue.autoUpdater.drain();
|
||||
|
||||
new Worker(
|
||||
'proxy',
|
||||
@@ -98,11 +102,22 @@ const cron = async (): Promise<void> => {
|
||||
}
|
||||
);
|
||||
|
||||
new Worker(
|
||||
'autoUpdater',
|
||||
async () => {
|
||||
await autoUpdater();
|
||||
},
|
||||
{
|
||||
...connectionOptions
|
||||
}
|
||||
);
|
||||
|
||||
await queue.proxy.add('proxy', {}, { repeat: { every: 10000 } });
|
||||
await queue.proxyTcpHttp.add('proxyTcpHttp', {}, { repeat: { every: 10000 } });
|
||||
await queue.ssl.add('ssl', {}, { repeat: { every: dev ? 10000 : 60000 } });
|
||||
if (!dev) await queue.cleanup.add('cleanup', {}, { repeat: { every: 300000 } });
|
||||
await queue.sslRenew.add('sslRenew', {}, { repeat: { every: 1800000 } });
|
||||
await queue.autoUpdater.add('autoUpdater', {}, { repeat: { every: 60000 } });
|
||||
};
|
||||
cron().catch((error) => {
|
||||
console.log('cron failed to start');
|
||||
@@ -115,6 +130,9 @@ const buildWorker = new Worker(buildQueueName, async (job) => await builder(job)
|
||||
concurrency: 1,
|
||||
...connectionOptions
|
||||
});
|
||||
buildQueue.resume().catch((err) => {
|
||||
console.log('Build queue failed to resume!', err);
|
||||
});
|
||||
|
||||
buildWorker.on('completed', async (job: Bullmq.Job) => {
|
||||
try {
|
||||
@@ -123,7 +141,6 @@ buildWorker.on('completed', async (job: Bullmq.Job) => {
|
||||
setTimeout(async () => {
|
||||
await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'success' } });
|
||||
}, 1234);
|
||||
console.log(error);
|
||||
} finally {
|
||||
const workdir = `/tmp/build-sources/${job.data.repository}/${job.data.build_id}`;
|
||||
if (!dev) await asyncExecShell(`rm -fr ${workdir}`);
|
||||
@@ -139,7 +156,6 @@ buildWorker.on('failed', async (job: Bullmq.Job, failedReason) => {
|
||||
setTimeout(async () => {
|
||||
await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'failed' } });
|
||||
}, 1234);
|
||||
console.log(error);
|
||||
} finally {
|
||||
const workdir = `/tmp/build-sources/${job.data.repository}`;
|
||||
if (!dev) await asyncExecShell(`rm -fr ${workdir}`);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
import { browser } from '$app/env';
|
||||
import { writable, type Writable, type Readable, readable } from 'svelte/store';
|
||||
|
||||
export const gitTokens: Writable<{ githubToken: string | null; gitlabToken: string | null }> =
|
||||
writable({
|
||||
@@ -6,3 +7,8 @@ export const gitTokens: Writable<{ githubToken: string | null; gitlabToken: stri
|
||||
gitlabToken: null
|
||||
});
|
||||
export const disabledButton: Writable<boolean> = writable(false);
|
||||
|
||||
export const features: Readable<{ latestVersion: string; beta: boolean }> = readable({
|
||||
beta: browser && window.localStorage.getItem('beta') === 'true',
|
||||
latestVersion: browser && window.localStorage.getItem('latestVersion')
|
||||
});
|
||||
|
||||
@@ -34,6 +34,8 @@ export type BuilderJob = {
|
||||
persistentStorage: { path: string }[];
|
||||
pullmergeRequestId?: unknown;
|
||||
sourceBranch?: string;
|
||||
baseImage: string;
|
||||
baseBuildImage: string;
|
||||
};
|
||||
|
||||
// TODO: Add the other build types
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
if (res.ok) {
|
||||
return {
|
||||
props: {
|
||||
selectedTeamId: session.teamId,
|
||||
...(await res.json())
|
||||
}
|
||||
};
|
||||
@@ -35,9 +34,6 @@
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export let teams;
|
||||
export let selectedTeamId;
|
||||
|
||||
import '../tailwind.css';
|
||||
import { SvelteToast, toast } from '@zerodevx/svelte-toast';
|
||||
import { page, session } from '$app/stores';
|
||||
@@ -45,7 +41,8 @@
|
||||
import { errorNotification } from '$lib/form';
|
||||
import { asyncSleep } from '$lib/components/common';
|
||||
import { del, get, post } from '$lib/api';
|
||||
import { browser, dev } from '$app/env';
|
||||
import { dev } from '$app/env';
|
||||
import { features } from '$lib/store';
|
||||
let isUpdateAvailable = false;
|
||||
|
||||
let updateStatus = {
|
||||
@@ -56,7 +53,7 @@
|
||||
let latestVersion = 'latest';
|
||||
onMount(async () => {
|
||||
if ($session.userId) {
|
||||
const overrideVersion = browser && window.localStorage.getItem('latestVersion');
|
||||
const overrideVersion = $features.latestVersion;
|
||||
try {
|
||||
await get(`/login.json`);
|
||||
} catch ({ error }) {
|
||||
@@ -89,17 +86,6 @@
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function switchTeam() {
|
||||
try {
|
||||
await post(`/dashboard.json?from=${$page.url.pathname}`, {
|
||||
cookie: 'teamId',
|
||||
value: selectedTeamId
|
||||
});
|
||||
return window.location.reload();
|
||||
} catch (error) {
|
||||
return window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
async function update() {
|
||||
updateStatus.loading = true;
|
||||
@@ -525,21 +511,10 @@
|
||||
</div>
|
||||
</nav>
|
||||
{#if $session.whiteLabeled}
|
||||
<span class="fixed bottom-0 left-[50px] z-50 m-2 px-4 text-xs text-stone-700"
|
||||
<span class="fixed bottom-0 left-[50px] z-50 m-2 px-4 text-xs text-stone-700"
|
||||
>Powered by <a href="https://coolify.io" target="_blank">Coolify</a></span
|
||||
>
|
||||
{/if}
|
||||
|
||||
<select
|
||||
class="fixed right-0 bottom-0 z-50 m-2 w-64 bg-opacity-30 p-2 px-4 hover:bg-opacity-100"
|
||||
bind:value={selectedTeamId}
|
||||
on:change={switchTeam}
|
||||
>
|
||||
<option value="" disabled selected>Switch to a different team...</option>
|
||||
{#each teams as team}
|
||||
<option value={team.teamId}>{team.team.name} - {team.permission}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
<main>
|
||||
<slot />
|
||||
|
||||
@@ -394,17 +394,17 @@
|
||||
>
|
||||
<div class="border border-coolgray-500 h-8" />
|
||||
<a
|
||||
href={!$disabledButton ? `/applications/${id}/logs` : null}
|
||||
href={!$disabledButton && isRunning ? `/applications/${id}/logs` : null}
|
||||
sveltekit:prefetch
|
||||
class="hover:text-sky-500 rounded"
|
||||
class:text-sky-500={$page.url.pathname === `/applications/${id}/logs`}
|
||||
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs`}
|
||||
>
|
||||
<button
|
||||
title={$t('application.build_logs')}
|
||||
disabled={$disabledButton}
|
||||
title={$t('application.logs')}
|
||||
disabled={$disabledButton || !isRunning}
|
||||
class="icons bg-transparent tooltip-bottom text-sm"
|
||||
data-tooltip={$t('application.build_logs')}
|
||||
data-tooltip={$t('application.logs')}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
75
src/routes/applications/[id]/cancel.json.ts
Normal file
75
src/routes/applications/[id]/cancel.json.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { asyncExecShell, getEngine, removeDestinationDocker, saveBuildLog } from '$lib/common';
|
||||
import { buildQueue } from '$lib/queues';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import * as db from '$lib/database';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { buildId, applicationId } = await event.request.json();
|
||||
if (!buildId) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
message: 'Build ID not found.'
|
||||
}
|
||||
};
|
||||
}
|
||||
try {
|
||||
let count = 0;
|
||||
await new Promise<void>(async (resolve, reject) => {
|
||||
const job = await buildQueue.getJob(buildId);
|
||||
const {
|
||||
destinationDocker: { engine }
|
||||
} = job.data;
|
||||
const host = getEngine(engine);
|
||||
let interval = setInterval(async () => {
|
||||
const { status } = await db.prisma.build.findUnique({ where: { id: buildId } });
|
||||
if (status === 'failed') {
|
||||
clearInterval(interval);
|
||||
return resolve();
|
||||
}
|
||||
if (count > 1200) {
|
||||
clearInterval(interval);
|
||||
reject(new Error('Could not cancel build.'));
|
||||
}
|
||||
try {
|
||||
const { stdout: buildContainers } = await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker container ls --filter "label=coolify.buildId=${buildId}" --format '{{json .}}'`
|
||||
);
|
||||
if (buildContainers) {
|
||||
const containersArray = buildContainers.trim().split('\n');
|
||||
for (const container of containersArray) {
|
||||
const containerObj = JSON.parse(container);
|
||||
const id = containerObj.ID;
|
||||
if (!containerObj.Names.startsWith(`${applicationId}`)) {
|
||||
await removeDestinationDocker({ id, engine });
|
||||
clearInterval(interval);
|
||||
await saveBuildLog({
|
||||
line: 'Canceled by user!',
|
||||
buildId: job.data.build_id,
|
||||
applicationId: job.data.id
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
count++;
|
||||
} catch (error) {}
|
||||
}, 100);
|
||||
|
||||
resolve();
|
||||
});
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
message: 'Build canceled.'
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
message: error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -185,7 +185,7 @@
|
||||
? $t('application.configuration.loading_repositories')
|
||||
: $t('application.configuration.select_a_repository')}
|
||||
id="repository"
|
||||
showIndicator={true}
|
||||
showIndicator={!loading.repositories}
|
||||
isWaiting={loading.repositories}
|
||||
on:select={loadBranches}
|
||||
items={reposSelectOptions}
|
||||
@@ -202,7 +202,7 @@
|
||||
? $t('application.configuration.select_a_repository_first')
|
||||
: $t('application.configuration.select_a_branch')}
|
||||
isWaiting={loading.branches}
|
||||
showIndicator={selected.repository}
|
||||
showIndicator={selected.repository && !loading.branches}
|
||||
id="branches"
|
||||
on:select={isBranchAlreadyUsed}
|
||||
items={branchSelectOptions}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { ErrorHandler } from '$lib/database';
|
||||
import { ErrorHandler, generatePassword } from '$lib/database';
|
||||
|
||||
export const get: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
@@ -34,6 +34,30 @@ export const post: RequestHandler = async (event) => {
|
||||
|
||||
try {
|
||||
await db.configureBuildPack({ id, buildPack });
|
||||
|
||||
// Generate default secrets
|
||||
if (buildPack === 'laravel') {
|
||||
let found = await db.isSecretExists({ id, name: 'APP_ENV', isPRMRSecret: false });
|
||||
if (!found) {
|
||||
await db.createSecret({
|
||||
id,
|
||||
name: 'APP_ENV',
|
||||
value: 'production',
|
||||
isBuildSecret: false,
|
||||
isPRMRSecret: false
|
||||
});
|
||||
}
|
||||
found = await db.isSecretExists({ id, name: 'APP_KEY', isPRMRSecret: false });
|
||||
if (!found) {
|
||||
await db.createSecret({
|
||||
id,
|
||||
name: 'APP_KEY',
|
||||
value: generatePassword(32),
|
||||
isBuildSecret: false,
|
||||
isPRMRSecret: false
|
||||
});
|
||||
}
|
||||
}
|
||||
return { status: 201 };
|
||||
} catch (error) {
|
||||
return ErrorHandler(error);
|
||||
|
||||
@@ -85,13 +85,14 @@
|
||||
const composerPHP = files.find(
|
||||
(file) => file.name === 'composer.json' && file.type === 'blob'
|
||||
);
|
||||
const laravel = files.find((file) => file.name === 'artisan' && file.type === 'blob');
|
||||
|
||||
if (yarnLock) packageManager = 'yarn';
|
||||
if (pnpmLock) packageManager = 'pnpm';
|
||||
|
||||
if (dockerfile) {
|
||||
foundConfig = findBuildPack('docker', packageManager);
|
||||
} else if (packageJson) {
|
||||
} else if (packageJson && !laravel) {
|
||||
const path = packageJson.path;
|
||||
const data = await get(
|
||||
`${apiUrl}/v4/projects/${projectId}/repository/files/${path}/raw?ref=${branch}`,
|
||||
@@ -107,8 +108,10 @@
|
||||
foundConfig = findBuildPack('python');
|
||||
} else if (indexHtml) {
|
||||
foundConfig = findBuildPack('static', packageManager);
|
||||
} else if (indexPHP || composerPHP) {
|
||||
} else if ((indexPHP || composerPHP) && !laravel) {
|
||||
foundConfig = findBuildPack('php');
|
||||
} else if (laravel) {
|
||||
foundConfig = findBuildPack('laravel');
|
||||
} else {
|
||||
foundConfig = findBuildPack('node', packageManager);
|
||||
}
|
||||
@@ -134,13 +137,14 @@
|
||||
const composerPHP = files.find(
|
||||
(file) => file.name === 'composer.json' && file.type === 'file'
|
||||
);
|
||||
const laravel = files.find((file) => file.name === 'artisan' && file.type === 'file');
|
||||
|
||||
if (yarnLock) packageManager = 'yarn';
|
||||
if (pnpmLock) packageManager = 'pnpm';
|
||||
|
||||
if (dockerfile) {
|
||||
foundConfig = findBuildPack('docker', packageManager);
|
||||
} else if (packageJson) {
|
||||
} else if (packageJson && !laravel) {
|
||||
const data = await get(`${packageJson.git_url}`, {
|
||||
Authorization: `Bearer ${$gitTokens.githubToken}`,
|
||||
Accept: 'application/vnd.github.v2.raw'
|
||||
@@ -153,8 +157,10 @@
|
||||
foundConfig = findBuildPack('python');
|
||||
} else if (indexHtml) {
|
||||
foundConfig = findBuildPack('static', packageManager);
|
||||
} else if (indexPHP || composerPHP) {
|
||||
} else if ((indexPHP || composerPHP) && !laravel) {
|
||||
foundConfig = findBuildPack('php');
|
||||
} else if (laravel) {
|
||||
foundConfig = findBuildPack('laravel');
|
||||
} else {
|
||||
foundConfig = findBuildPack('node', packageManager);
|
||||
}
|
||||
@@ -225,7 +231,7 @@
|
||||
<div class="max-w-7xl mx-auto flex flex-wrap justify-center">
|
||||
{#each buildPacks as buildPack}
|
||||
<div class="p-2">
|
||||
<BuildPack {buildPack} {scanning} bind:foundConfig />
|
||||
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -45,15 +45,23 @@ export const post: RequestHandler = async (event) => {
|
||||
}
|
||||
});
|
||||
if (pullmergeRequestId) {
|
||||
await buildQueue.add(buildId, {
|
||||
build_id: buildId,
|
||||
type: 'manual',
|
||||
...applicationFound,
|
||||
sourceBranch: branch,
|
||||
pullmergeRequestId
|
||||
});
|
||||
await buildQueue.add(
|
||||
buildId,
|
||||
{
|
||||
build_id: buildId,
|
||||
type: 'manual',
|
||||
...applicationFound,
|
||||
sourceBranch: branch,
|
||||
pullmergeRequestId
|
||||
},
|
||||
{ jobId: buildId }
|
||||
);
|
||||
} else {
|
||||
await buildQueue.add(buildId, { build_id: buildId, type: 'manual', ...applicationFound });
|
||||
await buildQueue.add(
|
||||
buildId,
|
||||
{ build_id: buildId, type: 'manual', ...applicationFound },
|
||||
{ jobId: buildId }
|
||||
);
|
||||
}
|
||||
return {
|
||||
status: 200,
|
||||
|
||||
@@ -62,7 +62,9 @@ export const post: RequestHandler = async (event) => {
|
||||
pythonVariable,
|
||||
dockerFileLocation,
|
||||
denoMainFile,
|
||||
denoOptions
|
||||
denoOptions,
|
||||
baseImage,
|
||||
baseBuildImage
|
||||
} = await event.request.json();
|
||||
if (port) port = Number(port);
|
||||
if (denoOptions) denoOptions = denoOptions.trim();
|
||||
@@ -96,6 +98,8 @@ export const post: RequestHandler = async (event) => {
|
||||
dockerFileLocation,
|
||||
denoMainFile,
|
||||
denoOptions,
|
||||
baseImage,
|
||||
baseBuildImage,
|
||||
...defaultConfiguration
|
||||
});
|
||||
return { status: 201 };
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
gitlabApp: Prisma.GitlabApp;
|
||||
gitSource: Prisma.GitSource;
|
||||
destinationDocker: Prisma.DestinationDocker;
|
||||
baseImages: Array<{ value: string; label: string }>;
|
||||
baseBuildImages: Array<{ value: string; label: string }>;
|
||||
};
|
||||
export let isRunning;
|
||||
import { page, session } from '$app/stores';
|
||||
@@ -71,11 +73,14 @@
|
||||
label: 'Gunicorn'
|
||||
}
|
||||
];
|
||||
|
||||
function containerClass() {
|
||||
if (!$session.isAdmin || isRunning) {
|
||||
return 'text-white border border-dashed border-coolgray-300 bg-transparent font-thin px-0';
|
||||
}
|
||||
}
|
||||
if (browser && window.location.hostname === 'demo.coolify.io' && !application.fqdn) {
|
||||
application.fqdn = `http://${cuid()}.demo.coolify.io`;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
domainEl.focus();
|
||||
});
|
||||
@@ -138,6 +143,14 @@
|
||||
async function selectWSGI(event) {
|
||||
application.pythonWSGI = event.detail.value;
|
||||
}
|
||||
async function selectBaseImage(event) {
|
||||
application.baseImage = event.detail.value;
|
||||
await handleSubmit();
|
||||
}
|
||||
async function selectBaseBuildImage(event) {
|
||||
application.baseBuildImage = event.detail.value;
|
||||
await handleSubmit();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
|
||||
@@ -310,6 +323,49 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="baseImage" class="text-base font-bold text-stone-100"
|
||||
>{$t('application.base_image')}</label
|
||||
>
|
||||
<div class="custom-select-wrapper">
|
||||
<Select
|
||||
isDisabled={!$session.isAdmin || isRunning}
|
||||
containerClasses={containerClass()}
|
||||
id="baseImages"
|
||||
showIndicator={!isRunning}
|
||||
items={application.baseImages}
|
||||
on:select={selectBaseImage}
|
||||
value={application.baseImage}
|
||||
isClearable={false}
|
||||
/>
|
||||
</div>
|
||||
<Explainer text={$t('application.base_image_explainer')} />
|
||||
</div>
|
||||
{#if application.buildCommand || application.buildPack === 'rust' || application.buildPack === 'laravel'}
|
||||
<div class="grid grid-cols-2 items-center pb-8">
|
||||
<label for="baseBuildImage" class="text-base font-bold text-stone-100"
|
||||
>{$t('application.base_build_image')}</label
|
||||
>
|
||||
|
||||
<div class="custom-select-wrapper">
|
||||
<Select
|
||||
isDisabled={!$session.isAdmin || isRunning}
|
||||
containerClasses={containerClass()}
|
||||
id="baseBuildImages"
|
||||
showIndicator={!isRunning}
|
||||
items={application.baseBuildImages}
|
||||
on:select={selectBaseBuildImage}
|
||||
value={application.baseBuildImage}
|
||||
isClearable={false}
|
||||
/>
|
||||
</div>
|
||||
{#if application.buildPack === 'laravel'}
|
||||
<Explainer text="For building frontend assets with webpack." />
|
||||
{:else}
|
||||
<Explainer text={$t('application.base_build_image_explainer')} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">{$t('application.application')}</div>
|
||||
@@ -476,21 +532,23 @@
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<div class="flex-col">
|
||||
<label for="baseDirectory" class="pt-2 text-base font-bold text-stone-100"
|
||||
>{$t('forms.base_directory')}</label
|
||||
>
|
||||
<Explainer text={$t('application.directory_to_use_explainer')} />
|
||||
{#if application.buildPack !== 'laravel'}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<div class="flex-col">
|
||||
<label for="baseDirectory" class="pt-2 text-base font-bold text-stone-100"
|
||||
>{$t('forms.base_directory')}</label
|
||||
>
|
||||
<Explainer text={$t('application.directory_to_use_explainer')} />
|
||||
</div>
|
||||
<input
|
||||
readonly={!$session.isAdmin}
|
||||
name="baseDirectory"
|
||||
id="baseDirectory"
|
||||
bind:value={application.baseDirectory}
|
||||
placeholder="{$t('forms.default')}: /"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
readonly={!$session.isAdmin}
|
||||
name="baseDirectory"
|
||||
id="baseDirectory"
|
||||
bind:value={application.baseDirectory}
|
||||
placeholder="{$t('forms.default')}: /"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if !notNodeDeployments.includes(application.buildPack)}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<div class="flex-col">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import Loading from '$lib/components/Loading.svelte';
|
||||
import LoadingLogs from '../_Loading.svelte';
|
||||
import { get } from '$lib/api';
|
||||
import { get, post } from '$lib/api';
|
||||
import { errorNotification } from '$lib/form';
|
||||
import { t } from '$lib/translations';
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
let followingInterval;
|
||||
let logsEl;
|
||||
|
||||
let cancelInprogress = false;
|
||||
|
||||
const { id } = $page.params;
|
||||
|
||||
const cleanAnsiCodes = (str: string) => str.replace(/\x1B\[(\d+)m/g, '');
|
||||
@@ -67,6 +69,19 @@
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function cancelBuild() {
|
||||
if (cancelInprogress) return;
|
||||
try {
|
||||
cancelInprogress = true;
|
||||
await post(`/applications/${id}/cancel.json`, {
|
||||
buildId,
|
||||
applicationId: id
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
onDestroy(() => {
|
||||
clearInterval(streamInterval);
|
||||
clearInterval(followingInterval);
|
||||
@@ -90,7 +105,7 @@
|
||||
<div class="flex justify-end sticky top-0 p-2">
|
||||
<button
|
||||
on:click={followBuild}
|
||||
class="bg-transparent"
|
||||
class="bg-transparent hover:text-green-500 hover:bg-coolgray-500"
|
||||
data-tooltip="Follow logs"
|
||||
class:text-green-500={followingBuild}
|
||||
>
|
||||
@@ -111,7 +126,30 @@
|
||||
<line x1="16" y1="12" x2="12" y2="16" />
|
||||
</svg>
|
||||
</button>
|
||||
{#if currentStatus === 'running'}
|
||||
<button
|
||||
on:click={cancelBuild}
|
||||
class="bg-transparent hover:text-red-500 hover:bg-coolgray-500"
|
||||
data-tooltip="Cancel build"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<path d="M10 10l4 4m0 -4l-4 4" />
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="font-mono leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
|
||||
bind:this={logsEl}
|
||||
|
||||
@@ -98,12 +98,12 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
|
||||
<div class="flex h-20 items-center space-x-2 p-5 px-6 font-bold">
|
||||
<div class="-mb-5 flex-col">
|
||||
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
|
||||
Application Logs
|
||||
</div>
|
||||
<span class="text-xs">{application.name} </span>
|
||||
<span class="text-xs">{application.name}</span>
|
||||
</div>
|
||||
|
||||
{#if application.fqdn}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
import Astro from '$lib/components/svg/applications/Astro.svelte';
|
||||
import Eleventy from '$lib/components/svg/applications/Eleventy.svelte';
|
||||
import Deno from '$lib/components/svg/applications/Deno.svelte';
|
||||
import Laravel from '$lib/components/svg/applications/Laravel.svelte';
|
||||
|
||||
async function newApplication() {
|
||||
const { id } = await post('/applications/new', {});
|
||||
@@ -104,6 +105,8 @@
|
||||
<Eleventy />
|
||||
{:else if application.buildPack.toLowerCase() === 'deno'}
|
||||
<Deno />
|
||||
{:else if application.buildPack.toLowerCase() === 'laravel'}
|
||||
<Laravel />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -162,6 +165,8 @@
|
||||
<Eleventy />
|
||||
{:else if application.buildPack.toLowerCase() === 'deno'}
|
||||
<Deno />
|
||||
{:else if application.buildPack.toLowerCase() === 'laravel'}
|
||||
<Laravel />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { session } from '$app/stores';
|
||||
import { page, session } from '$app/stores';
|
||||
import { errorNotification } from '$lib/form';
|
||||
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
|
||||
import Loading from '$lib/components/Loading.svelte';
|
||||
@@ -65,6 +65,8 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { t } from '$lib/translations';
|
||||
|
||||
const { id } = $page.params;
|
||||
|
||||
export let database;
|
||||
export let isRunning;
|
||||
let loading = false;
|
||||
@@ -163,6 +165,75 @@
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
<div class="border border-stone-700 h-8" />
|
||||
<a
|
||||
href="/databases/{id}"
|
||||
sveltekit:prefetch
|
||||
class="hover:text-yellow-500 rounded"
|
||||
class:text-yellow-500={$page.url.pathname === `/databases/${id}`}
|
||||
class:bg-coolgray-500={$page.url.pathname === `/databases/${id}`}
|
||||
>
|
||||
<button
|
||||
title={$t('application.configurations')}
|
||||
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
|
||||
data-tooltip={$t('application.configurations')}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<rect x="4" y="8" width="4" height="4" />
|
||||
<line x1="6" y1="4" x2="6" y2="8" />
|
||||
<line x1="6" y1="12" x2="6" y2="20" />
|
||||
<rect x="10" y="14" width="4" height="4" />
|
||||
<line x1="12" y1="4" x2="12" y2="14" />
|
||||
<line x1="12" y1="18" x2="12" y2="20" />
|
||||
<rect x="16" y="5" width="4" height="4" />
|
||||
<line x1="18" y1="4" x2="18" y2="5" />
|
||||
<line x1="18" y1="9" x2="18" y2="20" />
|
||||
</svg></button
|
||||
></a
|
||||
>
|
||||
<div class="border border-stone-700 h-8" />
|
||||
<a
|
||||
href={isRunning ? `/databases/${id}/logs` : null}
|
||||
sveltekit:prefetch
|
||||
class="hover:text-pink-500 rounded"
|
||||
class:text-pink-500={$page.url.pathname === `/databases/${id}/logs`}
|
||||
class:bg-coolgray-500={$page.url.pathname === `/databases/${id}/logs`}
|
||||
>
|
||||
<button
|
||||
title={$t('database.logs')}
|
||||
disabled={!isRunning}
|
||||
class="icons bg-transparent tooltip-bottom text-sm"
|
||||
data-tooltip={$t('database.logs')}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M3 19a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
|
||||
<path d="M3 6a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
|
||||
<line x1="3" y1="6" x2="3" y2="19" />
|
||||
<line x1="12" y1="6" x2="12" y2="19" />
|
||||
<line x1="21" y1="6" x2="21" y2="19" />
|
||||
</svg></button
|
||||
></a
|
||||
>
|
||||
<button
|
||||
on:click={deleteDatabase}
|
||||
title={$t('database.delete_database')}
|
||||
|
||||
41
src/routes/databases/[id]/logs/_Loading.svelte
Normal file
41
src/routes/databases/[id]/logs/_Loading.svelte
Normal file
@@ -0,0 +1,41 @@
|
||||
<div class="lds-ripple absolute left-0">
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.lds-ripple {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
left: -19px;
|
||||
top: -8px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
.lds-ripple div {
|
||||
position: absolute;
|
||||
border: 4px solid #fff;
|
||||
opacity: 1;
|
||||
border-radius: 50%;
|
||||
animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
|
||||
}
|
||||
.lds-ripple div:nth-child(2) {
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
@keyframes lds-ripple {
|
||||
0% {
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
66
src/routes/databases/[id]/logs/index.json.ts
Normal file
66
src/routes/databases/[id]/logs/index.json.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { ErrorHandler } from '$lib/database';
|
||||
import { dayjs } from '$lib/dayjs';
|
||||
import { dockerInstance } from '$lib/docker';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const get: RequestHandler = async (event) => {
|
||||
const { status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
let since = event.url.searchParams.get('since') || 0;
|
||||
if (since !== 0) {
|
||||
since = dayjs(since).unix();
|
||||
}
|
||||
try {
|
||||
const { destinationDockerId, destinationDocker } = await db.prisma.database.findUnique({
|
||||
where: { id },
|
||||
include: { destinationDocker: true }
|
||||
});
|
||||
if (destinationDockerId) {
|
||||
const docker = dockerInstance({ destinationDocker });
|
||||
try {
|
||||
const container = await docker.engine.getContainer(id);
|
||||
if (container) {
|
||||
const logs = (
|
||||
await container.logs({
|
||||
stdout: true,
|
||||
stderr: true,
|
||||
timestamps: true,
|
||||
since,
|
||||
tail: 5000
|
||||
})
|
||||
)
|
||||
.toString()
|
||||
.split('\n')
|
||||
.map((l) => l.slice(8))
|
||||
.filter((a) => a);
|
||||
return {
|
||||
body: {
|
||||
logs
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
const { statusCode } = error;
|
||||
if (statusCode === 404) {
|
||||
return {
|
||||
body: {
|
||||
logs: []
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
message: 'No logs found.'
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorHandler(error);
|
||||
}
|
||||
};
|
||||
179
src/routes/databases/[id]/logs/index.svelte
Normal file
179
src/routes/databases/[id]/logs/index.svelte
Normal file
@@ -0,0 +1,179 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
let endpoint = `/databases/${params.id}/logs.json`;
|
||||
const res = await fetch(endpoint);
|
||||
if (res.ok) {
|
||||
return {
|
||||
props: {
|
||||
database: stuff.database,
|
||||
...(await res.json())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: res.status,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let database;
|
||||
import { page } from '$app/stores';
|
||||
import LoadingLogs from './_Loading.svelte';
|
||||
import { get } from '$lib/api';
|
||||
import { errorNotification } from '$lib/form';
|
||||
import { t } from '$lib/translations';
|
||||
|
||||
let loadLogsInterval = null;
|
||||
let logs = [];
|
||||
let lastLog = null;
|
||||
let followingInterval;
|
||||
let followingLogs;
|
||||
let logsEl;
|
||||
let position = 0;
|
||||
|
||||
const { id } = $page.params;
|
||||
onMount(async () => {
|
||||
loadAllLogs();
|
||||
loadLogsInterval = setInterval(() => {
|
||||
loadLogs();
|
||||
}, 1000);
|
||||
});
|
||||
onDestroy(() => {
|
||||
clearInterval(loadLogsInterval);
|
||||
clearInterval(followingInterval);
|
||||
});
|
||||
async function loadAllLogs() {
|
||||
try {
|
||||
const data: any = await get(`/databases/${id}/logs.json`);
|
||||
if (data?.logs) {
|
||||
lastLog = data.logs[data.logs.length - 1];
|
||||
logs = data.logs;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function loadLogs() {
|
||||
try {
|
||||
const newLogs: any = await get(
|
||||
`/databases/${id}/logs.json?since=${lastLog?.split(' ')[0] || 0}`
|
||||
);
|
||||
|
||||
if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) {
|
||||
logs = logs.concat(newLogs.logs);
|
||||
lastLog = newLogs.logs[newLogs.logs.length - 1];
|
||||
}
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
function detect() {
|
||||
if (position < logsEl.scrollTop) {
|
||||
position = logsEl.scrollTop;
|
||||
} else {
|
||||
if (followingLogs) {
|
||||
clearInterval(followingInterval);
|
||||
followingLogs = false;
|
||||
}
|
||||
position = logsEl.scrollTop;
|
||||
}
|
||||
}
|
||||
|
||||
function followBuild() {
|
||||
followingLogs = !followingLogs;
|
||||
if (followingLogs) {
|
||||
followingInterval = setInterval(() => {
|
||||
logsEl.scrollTop = logsEl.scrollHeight;
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
}, 1000);
|
||||
} else {
|
||||
clearInterval(followingInterval);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-20 items-center space-x-2 p-5 px-6 font-bold">
|
||||
<div class="-mb-5 flex-col">
|
||||
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
|
||||
Database Logs
|
||||
</div>
|
||||
<span class="text-xs">{database.name}</span>
|
||||
</div>
|
||||
|
||||
{#if database.fqdn}
|
||||
<a
|
||||
href={database.fqdn}
|
||||
target="_blank"
|
||||
class="icons tooltip-bottom flex items-center bg-transparent text-sm"
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
|
||||
<line x1="10" y1="14" x2="20" y2="4" />
|
||||
<polyline points="15 4 20 4 20 9" />
|
||||
</svg></a
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-row justify-center space-x-2 px-10 pt-6">
|
||||
{#if logs.length === 0}
|
||||
<div class="text-xl font-bold tracking-tighter">{$t('application.build.waiting_logs')}</div>
|
||||
{:else}
|
||||
<div class="relative w-full">
|
||||
<div class="text-right " />
|
||||
{#if loadLogsInterval}
|
||||
<LoadingLogs />
|
||||
{/if}
|
||||
<div class="flex justify-end sticky top-0 p-2 mx-1">
|
||||
<button
|
||||
on:click={followBuild}
|
||||
class="bg-transparent"
|
||||
data-tooltip="Follow logs"
|
||||
class:text-green-500={followingLogs}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<line x1="8" y1="12" x2="12" y2="16" />
|
||||
<line x1="12" y1="8" x2="12" y2="16" />
|
||||
<line x1="16" y1="12" x2="12" y2="16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="font-mono w-full leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
|
||||
bind:this={logsEl}
|
||||
on:scroll={detect}
|
||||
>
|
||||
<div class="px-2 pr-14">
|
||||
{#each logs as log}
|
||||
{log + '\n'}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -25,7 +25,7 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { session } from '$app/stores';
|
||||
import { page, session } from '$app/stores';
|
||||
import { get, post } from '$lib/api';
|
||||
import { errorNotification } from '$lib/form';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
@@ -81,6 +81,18 @@
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function switchTeam(selectedTeamId) {
|
||||
try {
|
||||
await post(`/dashboard.json?from=${$page.url.pathname}`, {
|
||||
cookie: 'teamId',
|
||||
value: selectedTeamId
|
||||
});
|
||||
return window.location.reload();
|
||||
} catch (error) {
|
||||
return window.location.reload();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
@@ -175,20 +187,39 @@
|
||||
<div class="flex flex-row flex-wrap justify-center px-2 pb-10 md:flex-row">
|
||||
{#each ownTeams as team}
|
||||
<a href="/iam/team/{team.id}" class="w-96 p-2 no-underline">
|
||||
<div
|
||||
class="box-selection relative"
|
||||
class:hover:bg-fuchsia-600={team.id !== '0'}
|
||||
class:hover:bg-red-500={team.id === '0'}
|
||||
>
|
||||
<div class="truncate text-center text-xl font-bold">
|
||||
{team.name}
|
||||
<div class="box-selection relative">
|
||||
<div>
|
||||
<div class="truncate text-center text-xl font-bold">
|
||||
{team.name}
|
||||
</div>
|
||||
<div class="mt-1 text-center text-xs">
|
||||
{team.permissions?.length} member(s)
|
||||
</div>
|
||||
</div>
|
||||
<div class="truncate text-center font-bold">
|
||||
{team.id === '0' ? 'root team' : ''}
|
||||
</div>
|
||||
|
||||
<div class:mt-6={team.id !== '0'} class="mt-1 text-center">
|
||||
{team.permissions?.length} member(s)
|
||||
<div class="flex items-center justify-center pt-3">
|
||||
<button
|
||||
on:click|preventDefault={() => switchTeam(team.id)}
|
||||
class:bg-fuchsia-600={$session.teamId !== team.id}
|
||||
class:hover:bg-fuchsia-500={$session.teamId !== team.id}
|
||||
class:bg-transparent={$session.teamId === team.id}
|
||||
disabled={$session.teamId === team.id}
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M3 17h5l1.67 -2.386m3.66 -5.227l1.67 -2.387h6" />
|
||||
<path d="M18 4l3 3l-3 3" />
|
||||
<path d="M3 7h5l7 10h6" />
|
||||
<path d="M18 20l3 -3l-3 -3" />
|
||||
</svg></button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@@ -207,9 +238,6 @@
|
||||
<div class="truncate text-center text-xl font-bold">
|
||||
{team.name}
|
||||
</div>
|
||||
<div class="truncate text-center font-bold">
|
||||
{team.id === '0' ? 'root team' : ''}
|
||||
</div>
|
||||
|
||||
<div class="mt-1 text-center">{team.permissions?.length} member(s)</div>
|
||||
</div>
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import * as db from '$lib/database';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const data = await event.request.json();
|
||||
for (const d of data) {
|
||||
if (d.container_name) {
|
||||
const { log, container_name: containerId, source } = d;
|
||||
console.log(log);
|
||||
// await db.prisma.applicationLogs.create({ data: { log, containerId: containerId.substr(1), source } });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {}
|
||||
};
|
||||
};
|
||||
184
src/routes/services/[id]/_Services/_Fider.svelte
Normal file
184
src/routes/services/[id]/_Services/_Fider.svelte
Normal file
@@ -0,0 +1,184 @@
|
||||
<script lang="ts">
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import { t } from '$lib/translations';
|
||||
import Select from 'svelte-select';
|
||||
export let service;
|
||||
export let readOnly;
|
||||
|
||||
let mailgunRegions = [
|
||||
{
|
||||
value: 'EU',
|
||||
label: 'EU'
|
||||
},
|
||||
{
|
||||
value: 'US',
|
||||
label: 'US'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Fider</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="jwtSecret">JWT Secret</label>
|
||||
<CopyPasswordField
|
||||
name="jwtSecret"
|
||||
id="jwtSecret"
|
||||
isPasswordField
|
||||
value={service.fider.jwtSecret}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailNoreply">Noreply Email</label>
|
||||
<input
|
||||
name="emailNoreply"
|
||||
id="emailNoreply"
|
||||
type="email"
|
||||
required
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
bind:value={service.fider.emailNoreply}
|
||||
placeholder="{$t('forms.eg')}: noreply@yourdomain.com"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Email</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailMailgunApiKey">Mailgun API Key</label>
|
||||
<CopyPasswordField
|
||||
name="emailMailgunApiKey"
|
||||
id="emailMailgunApiKey"
|
||||
isPasswordField
|
||||
bind:value={service.fider.emailMailgunApiKey}
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
placeholder="{$t('forms.eg')}: key-yourkeygoeshere"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailMailgunDomain">Mailgun Domain</label>
|
||||
<input
|
||||
name="emailMailgunDomain"
|
||||
id="emailMailgunDomain"
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
bind:value={service.fider.emailMailgunDomain}
|
||||
placeholder="{$t('forms.eg')}: yourdomain.com"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailMailgunRegion">Mailgun Region</label>
|
||||
<div class="custom-select-wrapper">
|
||||
<Select
|
||||
id="baseBuildImages"
|
||||
items={mailgunRegions}
|
||||
showIndicator
|
||||
on:select={(event) => (service.fider.emailMailgunRegion = event.detail.value)}
|
||||
value={service.fider.emailMailgunRegion || 'EU'}
|
||||
isClearable={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-1 py-5 px-10 font-bold">
|
||||
<div class="text-lg">Or</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailSmtpHost">SMTP Host</label>
|
||||
<input
|
||||
name="emailSmtpHost"
|
||||
id="emailSmtpHost"
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
bind:value={service.fider.emailSmtpHost}
|
||||
placeholder="{$t('forms.eg')}: smtp.yourdomain.com"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailSmtpPort">SMTP Port</label>
|
||||
<input
|
||||
name="emailSmtpPort"
|
||||
id="emailSmtpPort"
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
bind:value={service.fider.emailSmtpPort}
|
||||
placeholder="{$t('forms.eg')}: 587"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailSmtpUser">SMTP User</label>
|
||||
<input
|
||||
name="emailSmtpUser"
|
||||
id="emailSmtpUser"
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
bind:value={service.fider.emailSmtpUser}
|
||||
placeholder="{$t('forms.eg')}: user@yourdomain.com"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailSmtpPassword">SMTP Password</label>
|
||||
<CopyPasswordField
|
||||
name="emailSmtpPassword"
|
||||
id="emailSmtpPassword"
|
||||
isPasswordField
|
||||
value={service.fider.emailSmtpPassword}
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
placeholder="{$t('forms.eg')}: s0m3p4ssw0rd"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailSmtpEnableStartTls">SMTP Start TLS</label>
|
||||
<input
|
||||
name="emailSmtpEnableStartTls"
|
||||
id="emailSmtpEnableStartTls"
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
bind:value={service.fider.emailSmtpEnableStartTls}
|
||||
placeholder="{$t('forms.eg')}: true"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">PostgreSQL</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="postgresqlUser">{$t('forms.username')}</label>
|
||||
<CopyPasswordField
|
||||
name="postgresqlUser"
|
||||
id="postgresqlUser"
|
||||
value={service.fider.postgresqlUser}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="postgresqlPassword">{$t('forms.password')}</label>
|
||||
<CopyPasswordField
|
||||
id="postgresqlPassword"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="postgresqlPassword"
|
||||
value={service.fider.postgresqlPassword}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="postgresqlDatabase">{$t('index.database')}</label>
|
||||
<CopyPasswordField
|
||||
name="postgresqlDatabase"
|
||||
id="postgresqlDatabase"
|
||||
value={service.fider.postgresqlDatabase}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
58
src/routes/services/[id]/_Services/_Hasura.svelte
Normal file
58
src/routes/services/[id]/_Services/_Hasura.svelte
Normal file
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import { t } from '$lib/translations';
|
||||
export let service;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Hasura</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="graphQLAdminPassword">GraphQL Admin Password</label>
|
||||
<CopyPasswordField
|
||||
name="graphQLAdminPassword"
|
||||
id="graphQLAdminPassword"
|
||||
isPasswordField
|
||||
value={service.hasura.graphQLAdminPassword}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">PostgreSQL</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="postgresqlUser">{$t('forms.username')}</label>
|
||||
<CopyPasswordField
|
||||
name="postgresqlUser"
|
||||
id="postgresqlUser"
|
||||
value={service.hasura.postgresqlUser}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="postgresqlPassword">{$t('forms.password')}</label>
|
||||
<CopyPasswordField
|
||||
id="postgresqlPassword"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="postgresqlPassword"
|
||||
value={service.hasura.postgresqlPassword}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="postgresqlDatabase">{$t('index.database')}</label>
|
||||
<CopyPasswordField
|
||||
name="postgresqlDatabase"
|
||||
id="postgresqlDatabase"
|
||||
value={service.hasura.postgresqlDatabase}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
@@ -12,10 +12,13 @@
|
||||
import { errorNotification } from '$lib/form';
|
||||
import { t } from '$lib/translations';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import Fider from './_Fider.svelte';
|
||||
import Ghost from './_Ghost.svelte';
|
||||
import Hasura from './_Hasura.svelte';
|
||||
import MeiliSearch from './_MeiliSearch.svelte';
|
||||
import MinIo from './_MinIO.svelte';
|
||||
import PlausibleAnalytics from './_PlausibleAnalytics.svelte';
|
||||
import Umami from './_Umami.svelte';
|
||||
import VsCodeServer from './_VSCodeServer.svelte';
|
||||
import Wordpress from './_Wordpress.svelte';
|
||||
|
||||
@@ -169,6 +172,12 @@
|
||||
<Ghost bind:service {readOnly} />
|
||||
{:else if service.type === 'meilisearch'}
|
||||
<MeiliSearch bind:service />
|
||||
{:else if service.type === 'umami'}
|
||||
<Umami bind:service />
|
||||
{:else if service.type === 'hasura'}
|
||||
<Hasura bind:service />
|
||||
{:else if service.type === 'fider'}
|
||||
<Fider bind:service {readOnly} />
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
29
src/routes/services/[id]/_Services/_Umami.svelte
Normal file
29
src/routes/services/[id]/_Services/_Umami.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
|
||||
export let service;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Umami</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="adminUser">Admin User</label>
|
||||
<input name="adminUser" id="adminUser" placeholder="admin" value="admin" disabled readonly />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="umamiAdminPassword">Initial Admin Password</label>
|
||||
<CopyPasswordField
|
||||
isPasswordField
|
||||
name="umamiAdminPassword"
|
||||
id="umamiAdminPassword"
|
||||
placeholder="admin"
|
||||
value={service.umami.umamiAdminPassword}
|
||||
disabled
|
||||
readonly
|
||||
/>
|
||||
<Explainer
|
||||
text="It could be changed in Umami. <br>This is just the password set initially after the first start."
|
||||
/>
|
||||
</div>
|
||||
@@ -270,6 +270,38 @@
|
||||
</button></a
|
||||
>
|
||||
<div class="border border-stone-700 h-8" />
|
||||
<a
|
||||
href={isRunning ? `/services/${id}/logs` : null}
|
||||
sveltekit:prefetch
|
||||
class="hover:text-pink-500 rounded"
|
||||
class:text-pink-500={$page.url.pathname === `/services/${id}/logs`}
|
||||
class:bg-coolgray-500={$page.url.pathname === `/services/${id}/logs`}
|
||||
>
|
||||
<button
|
||||
title={$t('service.logs')}
|
||||
disabled={!isRunning}
|
||||
class="icons bg-transparent tooltip-bottom text-sm"
|
||||
data-tooltip={$t('service.logs')}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M3 19a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
|
||||
<path d="M3 6a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
|
||||
<line x1="3" y1="6" x2="3" y2="19" />
|
||||
<line x1="12" y1="6" x2="12" y2="19" />
|
||||
<line x1="21" y1="6" x2="21" y2="19" />
|
||||
</svg></button
|
||||
></a
|
||||
>
|
||||
{/if}
|
||||
<button
|
||||
on:click={deleteService}
|
||||
|
||||
@@ -43,6 +43,9 @@
|
||||
import Ghost from '$lib/components/svg/services/Ghost.svelte';
|
||||
import { t } from '$lib/translations';
|
||||
import MeiliSearch from '$lib/components/svg/services/MeiliSearch.svelte';
|
||||
import Umami from '$lib/components/svg/services/Umami.svelte';
|
||||
import Hasura from '$lib/components/svg/services/Hasura.svelte';
|
||||
import Fider from '$lib/components/svg/services/Fider.svelte';
|
||||
|
||||
const { id } = $page.params;
|
||||
const from = $page.url.searchParams.get('from');
|
||||
@@ -90,6 +93,12 @@
|
||||
<Ghost isAbsolute />
|
||||
{:else if type.name === 'meilisearch'}
|
||||
<MeiliSearch isAbsolute />
|
||||
{:else if type.name === 'umami'}
|
||||
<Umami isAbsolute />
|
||||
{:else if type.name === 'hasura'}
|
||||
<Hasura isAbsolute />
|
||||
{:else if type.name === 'fider'}
|
||||
<Fider isAbsolute />
|
||||
{/if}{type.fancyName}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
57
src/routes/services/[id]/fider/index.json.ts
Normal file
57
src/routes/services/[id]/fider/index.json.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import { encrypt } from '$lib/crypto';
|
||||
import * as db from '$lib/database';
|
||||
import { ErrorHandler } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
|
||||
let {
|
||||
name,
|
||||
fqdn,
|
||||
fider: {
|
||||
emailNoreply,
|
||||
emailMailgunApiKey,
|
||||
emailMailgunDomain,
|
||||
emailMailgunRegion,
|
||||
emailSmtpHost,
|
||||
emailSmtpPort,
|
||||
emailSmtpUser,
|
||||
emailSmtpPassword,
|
||||
emailSmtpEnableStartTls
|
||||
}
|
||||
} = await event.request.json();
|
||||
|
||||
if (fqdn) fqdn = fqdn.toLowerCase();
|
||||
if (emailNoreply) emailNoreply = emailNoreply.toLowerCase();
|
||||
if (emailSmtpHost) emailSmtpHost = emailSmtpHost.toLowerCase();
|
||||
if (emailSmtpPassword) {
|
||||
emailSmtpPassword = encrypt(emailSmtpPassword);
|
||||
}
|
||||
if (emailSmtpPort) emailSmtpPort = Number(emailSmtpPort);
|
||||
if (emailSmtpEnableStartTls) emailSmtpEnableStartTls = Boolean(emailSmtpEnableStartTls);
|
||||
|
||||
try {
|
||||
await db.updateFiderService({
|
||||
id,
|
||||
fqdn,
|
||||
name,
|
||||
emailNoreply,
|
||||
emailMailgunApiKey,
|
||||
emailMailgunDomain,
|
||||
emailMailgunRegion,
|
||||
emailSmtpHost,
|
||||
emailSmtpPort,
|
||||
emailSmtpUser,
|
||||
emailSmtpPassword,
|
||||
emailSmtpEnableStartTls
|
||||
});
|
||||
return { status: 201 };
|
||||
} catch (error) {
|
||||
return ErrorHandler(error);
|
||||
}
|
||||
};
|
||||
154
src/routes/services/[id]/fider/start.json.ts
Normal file
154
src/routes/services/[id]/fider/start.json.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import {
|
||||
asyncExecShell,
|
||||
createDirectories,
|
||||
getDomain,
|
||||
getEngine,
|
||||
getUserDetails
|
||||
} from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { promises as fs } from 'fs';
|
||||
import yaml from 'js-yaml';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { ErrorHandler, getServiceImage } from '$lib/database';
|
||||
import { makeLabelForServices } from '$lib/buildPacks/common';
|
||||
import type { ComposeFile } from '$lib/types/composeFile';
|
||||
import type { Service, DestinationDocker, Prisma } from '@prisma/client';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
|
||||
try {
|
||||
const service: Service & Prisma.ServiceInclude & { destinationDocker: DestinationDocker } =
|
||||
await db.getService({ id, teamId });
|
||||
const {
|
||||
type,
|
||||
version,
|
||||
fqdn,
|
||||
destinationDockerId,
|
||||
destinationDocker,
|
||||
serviceSecret,
|
||||
fider: {
|
||||
postgresqlUser,
|
||||
postgresqlPassword,
|
||||
postgresqlDatabase,
|
||||
jwtSecret,
|
||||
emailNoreply,
|
||||
emailMailgunApiKey,
|
||||
emailMailgunDomain,
|
||||
emailMailgunRegion,
|
||||
emailSmtpHost,
|
||||
emailSmtpPort,
|
||||
emailSmtpUser,
|
||||
emailSmtpPassword,
|
||||
emailSmtpEnableStartTls
|
||||
}
|
||||
} = service;
|
||||
const network = destinationDockerId && destinationDocker.network;
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
const image = getServiceImage(type);
|
||||
const domain = getDomain(fqdn);
|
||||
const config = {
|
||||
fider: {
|
||||
image: `${image}:${version}`,
|
||||
environmentVariables: {
|
||||
HOST_DOMAIN: domain,
|
||||
DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}?sslmode=disable`,
|
||||
JWT_SECRET: `${jwtSecret.replace(/\$/g, '$$$')}`,
|
||||
EMAIL_NOREPLY: emailNoreply,
|
||||
EMAIL_MAILGUN_API: emailMailgunApiKey,
|
||||
EMAIL_MAILGUN_REGION: emailMailgunRegion,
|
||||
EMAIL_MAILGUN_DOMAIN: emailMailgunDomain,
|
||||
EMAIL_SMTP_HOST: emailSmtpHost,
|
||||
EMAIL_SMTP_PORT: emailSmtpPort,
|
||||
EMAIL_SMTP_USER: emailSmtpUser,
|
||||
EMAIL_SMTP_PASSWORD: emailSmtpPassword,
|
||||
EMAIL_SMTP_ENABLE_STARTTLS: emailSmtpEnableStartTls
|
||||
}
|
||||
},
|
||||
postgresql: {
|
||||
image: 'postgres:12-alpine',
|
||||
volume: `${id}-postgresql-data:/var/lib/postgresql/data`,
|
||||
environmentVariables: {
|
||||
POSTGRES_USER: postgresqlUser,
|
||||
POSTGRES_PASSWORD: postgresqlPassword,
|
||||
POSTGRES_DB: postgresqlDatabase
|
||||
}
|
||||
}
|
||||
};
|
||||
if (serviceSecret.length > 0) {
|
||||
serviceSecret.forEach((secret) => {
|
||||
config.fider.environmentVariables[secret.name] = secret.value;
|
||||
});
|
||||
}
|
||||
|
||||
const composeFile: ComposeFile = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[id]: {
|
||||
container_name: id,
|
||||
image: config.fider.image,
|
||||
environment: config.fider.environmentVariables,
|
||||
networks: [network],
|
||||
volumes: [],
|
||||
restart: 'always',
|
||||
labels: makeLabelForServices('fider'),
|
||||
deploy: {
|
||||
restart_policy: {
|
||||
condition: 'on-failure',
|
||||
delay: '5s',
|
||||
max_attempts: 3,
|
||||
window: '120s'
|
||||
}
|
||||
},
|
||||
depends_on: [`${id}-postgresql`]
|
||||
},
|
||||
[`${id}-postgresql`]: {
|
||||
image: config.postgresql.image,
|
||||
container_name: `${id}-postgresql`,
|
||||
environment: config.postgresql.environmentVariables,
|
||||
networks: [network],
|
||||
volumes: [config.postgresql.volume],
|
||||
restart: 'always',
|
||||
deploy: {
|
||||
restart_policy: {
|
||||
condition: 'on-failure',
|
||||
delay: '5s',
|
||||
max_attempts: 3,
|
||||
window: '120s'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
[network]: {
|
||||
external: true
|
||||
}
|
||||
},
|
||||
volumes: {
|
||||
[config.postgresql.volume.split(':')[0]]: {
|
||||
name: config.postgresql.volume.split(':')[0]
|
||||
}
|
||||
}
|
||||
};
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
|
||||
try {
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
|
||||
return {
|
||||
status: 200
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return ErrorHandler(error);
|
||||
}
|
||||
} catch (error) {
|
||||
return ErrorHandler(error);
|
||||
}
|
||||
};
|
||||
42
src/routes/services/[id]/fider/stop.json.ts
Normal file
42
src/routes/services/[id]/fider/stop.json.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { getUserDetails, removeDestinationDocker } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { ErrorHandler } from '$lib/database';
|
||||
import { checkContainer, stopTcpHttpProxy } from '$lib/haproxy';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
|
||||
try {
|
||||
const service = await db.getService({ id, teamId });
|
||||
const { destinationDockerId, destinationDocker } = service;
|
||||
if (destinationDockerId) {
|
||||
const engine = destinationDocker.engine;
|
||||
|
||||
try {
|
||||
const found = await checkContainer(engine, id);
|
||||
if (found) {
|
||||
await removeDestinationDocker({ id, engine });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
try {
|
||||
const found = await checkContainer(engine, `${id}-postgresql`);
|
||||
if (found) {
|
||||
await removeDestinationDocker({ id: `${id}-postgresql`, engine });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: 200
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorHandler(error);
|
||||
}
|
||||
};
|
||||
21
src/routes/services/[id]/hasura/index.json.ts
Normal file
21
src/routes/services/[id]/hasura/index.json.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { ErrorHandler } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
|
||||
let { name, fqdn } = await event.request.json();
|
||||
if (fqdn) fqdn = fqdn.toLowerCase();
|
||||
|
||||
try {
|
||||
await db.updateService({ id, fqdn, name });
|
||||
return { status: 201 };
|
||||
} catch (error) {
|
||||
return ErrorHandler(error);
|
||||
}
|
||||
};
|
||||
122
src/routes/services/[id]/hasura/start.json.ts
Normal file
122
src/routes/services/[id]/hasura/start.json.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { asyncExecShell, createDirectories, getEngine, getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { promises as fs } from 'fs';
|
||||
import yaml from 'js-yaml';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { ErrorHandler, getServiceImage } from '$lib/database';
|
||||
import { makeLabelForServices } from '$lib/buildPacks/common';
|
||||
import type { ComposeFile } from '$lib/types/composeFile';
|
||||
import type { Service, DestinationDocker, Prisma } from '@prisma/client';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
|
||||
try {
|
||||
const service: Service & Prisma.ServiceInclude & { destinationDocker: DestinationDocker } =
|
||||
await db.getService({ id, teamId });
|
||||
const {
|
||||
type,
|
||||
version,
|
||||
destinationDockerId,
|
||||
destinationDocker,
|
||||
serviceSecret,
|
||||
hasura: { postgresqlUser, postgresqlPassword, postgresqlDatabase }
|
||||
} = service;
|
||||
const network = destinationDockerId && destinationDocker.network;
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
const image = getServiceImage(type);
|
||||
|
||||
const config = {
|
||||
hasura: {
|
||||
image: `${image}:${version}`,
|
||||
environmentVariables: {
|
||||
HASURA_GRAPHQL_METADATA_DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}`
|
||||
}
|
||||
},
|
||||
postgresql: {
|
||||
image: 'postgres:12-alpine',
|
||||
volume: `${id}-postgresql-data:/var/lib/postgresql/data`,
|
||||
environmentVariables: {
|
||||
POSTGRES_USER: postgresqlUser,
|
||||
POSTGRES_PASSWORD: postgresqlPassword,
|
||||
POSTGRES_DB: postgresqlDatabase
|
||||
}
|
||||
}
|
||||
};
|
||||
if (serviceSecret.length > 0) {
|
||||
serviceSecret.forEach((secret) => {
|
||||
config.hasura.environmentVariables[secret.name] = secret.value;
|
||||
});
|
||||
}
|
||||
|
||||
const composeFile: ComposeFile = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[id]: {
|
||||
container_name: id,
|
||||
image: config.hasura.image,
|
||||
environment: config.hasura.environmentVariables,
|
||||
networks: [network],
|
||||
volumes: [],
|
||||
restart: 'always',
|
||||
labels: makeLabelForServices('hasura'),
|
||||
deploy: {
|
||||
restart_policy: {
|
||||
condition: 'on-failure',
|
||||
delay: '5s',
|
||||
max_attempts: 3,
|
||||
window: '120s'
|
||||
}
|
||||
},
|
||||
depends_on: [`${id}-postgresql`]
|
||||
},
|
||||
[`${id}-postgresql`]: {
|
||||
image: config.postgresql.image,
|
||||
container_name: `${id}-postgresql`,
|
||||
environment: config.postgresql.environmentVariables,
|
||||
networks: [network],
|
||||
volumes: [config.postgresql.volume],
|
||||
restart: 'always',
|
||||
deploy: {
|
||||
restart_policy: {
|
||||
condition: 'on-failure',
|
||||
delay: '5s',
|
||||
max_attempts: 3,
|
||||
window: '120s'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
[network]: {
|
||||
external: true
|
||||
}
|
||||
},
|
||||
volumes: {
|
||||
[config.postgresql.volume.split(':')[0]]: {
|
||||
name: config.postgresql.volume.split(':')[0]
|
||||
}
|
||||
}
|
||||
};
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
|
||||
try {
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
|
||||
return {
|
||||
status: 200
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return ErrorHandler(error);
|
||||
}
|
||||
} catch (error) {
|
||||
return ErrorHandler(error);
|
||||
}
|
||||
};
|
||||
42
src/routes/services/[id]/hasura/stop.json.ts
Normal file
42
src/routes/services/[id]/hasura/stop.json.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { getUserDetails, removeDestinationDocker } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { ErrorHandler } from '$lib/database';
|
||||
import { checkContainer, stopTcpHttpProxy } from '$lib/haproxy';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
|
||||
try {
|
||||
const service = await db.getService({ id, teamId });
|
||||
const { destinationDockerId, destinationDocker } = service;
|
||||
if (destinationDockerId) {
|
||||
const engine = destinationDocker.engine;
|
||||
|
||||
try {
|
||||
const found = await checkContainer(engine, id);
|
||||
if (found) {
|
||||
await removeDestinationDocker({ id, engine });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
try {
|
||||
const found = await checkContainer(engine, `${id}-postgresql`);
|
||||
if (found) {
|
||||
await removeDestinationDocker({ id: `${id}-postgresql`, engine });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: 200
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorHandler(error);
|
||||
}
|
||||
};
|
||||
41
src/routes/services/[id]/logs/_Loading.svelte
Normal file
41
src/routes/services/[id]/logs/_Loading.svelte
Normal file
@@ -0,0 +1,41 @@
|
||||
<div class="lds-ripple absolute left-0">
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.lds-ripple {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
left: -19px;
|
||||
top: -8px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
.lds-ripple div {
|
||||
position: absolute;
|
||||
border: 4px solid #fff;
|
||||
opacity: 1;
|
||||
border-radius: 50%;
|
||||
animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
|
||||
}
|
||||
.lds-ripple div:nth-child(2) {
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
@keyframes lds-ripple {
|
||||
0% {
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
66
src/routes/services/[id]/logs/index.json.ts
Normal file
66
src/routes/services/[id]/logs/index.json.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { ErrorHandler } from '$lib/database';
|
||||
import { dayjs } from '$lib/dayjs';
|
||||
import { dockerInstance } from '$lib/docker';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const get: RequestHandler = async (event) => {
|
||||
const { status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
let since = event.url.searchParams.get('since') || 0;
|
||||
if (since !== 0) {
|
||||
since = dayjs(since).unix();
|
||||
}
|
||||
try {
|
||||
const { destinationDockerId, destinationDocker } = await db.prisma.service.findUnique({
|
||||
where: { id },
|
||||
include: { destinationDocker: true }
|
||||
});
|
||||
if (destinationDockerId) {
|
||||
const docker = dockerInstance({ destinationDocker });
|
||||
try {
|
||||
const container = await docker.engine.getContainer(id);
|
||||
if (container) {
|
||||
const logs = (
|
||||
await container.logs({
|
||||
stdout: true,
|
||||
stderr: true,
|
||||
timestamps: true,
|
||||
since,
|
||||
tail: 5000
|
||||
})
|
||||
)
|
||||
.toString()
|
||||
.split('\n')
|
||||
.map((l) => l.slice(8))
|
||||
.filter((a) => a);
|
||||
return {
|
||||
body: {
|
||||
logs
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
const { statusCode } = error;
|
||||
if (statusCode === 404) {
|
||||
return {
|
||||
body: {
|
||||
logs: []
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
message: 'No logs found.'
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorHandler(error);
|
||||
}
|
||||
};
|
||||
179
src/routes/services/[id]/logs/index.svelte
Normal file
179
src/routes/services/[id]/logs/index.svelte
Normal file
@@ -0,0 +1,179 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
let endpoint = `/services/${params.id}/logs.json`;
|
||||
const res = await fetch(endpoint);
|
||||
if (res.ok) {
|
||||
return {
|
||||
props: {
|
||||
service: stuff.service,
|
||||
...(await res.json())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: res.status,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let service;
|
||||
import { page } from '$app/stores';
|
||||
import LoadingLogs from './_Loading.svelte';
|
||||
import { get } from '$lib/api';
|
||||
import { errorNotification } from '$lib/form';
|
||||
import { t } from '$lib/translations';
|
||||
|
||||
let loadLogsInterval = null;
|
||||
let logs = [];
|
||||
let lastLog = null;
|
||||
let followingInterval;
|
||||
let followingLogs;
|
||||
let logsEl;
|
||||
let position = 0;
|
||||
|
||||
const { id } = $page.params;
|
||||
onMount(async () => {
|
||||
loadAllLogs();
|
||||
loadLogsInterval = setInterval(() => {
|
||||
loadLogs();
|
||||
}, 1000);
|
||||
});
|
||||
onDestroy(() => {
|
||||
clearInterval(loadLogsInterval);
|
||||
clearInterval(followingInterval);
|
||||
});
|
||||
async function loadAllLogs() {
|
||||
try {
|
||||
const data: any = await get(`/services/${id}/logs.json`);
|
||||
if (data?.logs) {
|
||||
lastLog = data.logs[data.logs.length - 1];
|
||||
logs = data.logs;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function loadLogs() {
|
||||
try {
|
||||
const newLogs: any = await get(
|
||||
`/services/${id}/logs.json?since=${lastLog?.split(' ')[0] || 0}`
|
||||
);
|
||||
|
||||
if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) {
|
||||
logs = logs.concat(newLogs.logs);
|
||||
lastLog = newLogs.logs[newLogs.logs.length - 1];
|
||||
}
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
function detect() {
|
||||
if (position < logsEl.scrollTop) {
|
||||
position = logsEl.scrollTop;
|
||||
} else {
|
||||
if (followingLogs) {
|
||||
clearInterval(followingInterval);
|
||||
followingLogs = false;
|
||||
}
|
||||
position = logsEl.scrollTop;
|
||||
}
|
||||
}
|
||||
|
||||
function followBuild() {
|
||||
followingLogs = !followingLogs;
|
||||
if (followingLogs) {
|
||||
followingInterval = setInterval(() => {
|
||||
logsEl.scrollTop = logsEl.scrollHeight;
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
}, 1000);
|
||||
} else {
|
||||
clearInterval(followingInterval);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-20 items-center space-x-2 p-5 px-6 font-bold">
|
||||
<div class="-mb-5 flex-col">
|
||||
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
|
||||
Service Logs
|
||||
</div>
|
||||
<span class="text-xs">{service.name}</span>
|
||||
</div>
|
||||
|
||||
{#if service.fqdn}
|
||||
<a
|
||||
href={service.fqdn}
|
||||
target="_blank"
|
||||
class="icons tooltip-bottom flex items-center bg-transparent text-sm"
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
|
||||
<line x1="10" y1="14" x2="20" y2="4" />
|
||||
<polyline points="15 4 20 4 20 9" />
|
||||
</svg></a
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-row justify-center space-x-2 px-10 pt-6">
|
||||
{#if logs.length === 0}
|
||||
<div class="text-xl font-bold tracking-tighter">{$t('application.build.waiting_logs')}</div>
|
||||
{:else}
|
||||
<div class="relative w-full">
|
||||
<div class="text-right " />
|
||||
{#if loadLogsInterval}
|
||||
<LoadingLogs />
|
||||
{/if}
|
||||
<div class="flex justify-end sticky top-0 p-2 mx-1">
|
||||
<button
|
||||
on:click={followBuild}
|
||||
class="bg-transparent"
|
||||
data-tooltip="Follow logs"
|
||||
class:text-green-500={followingLogs}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<line x1="8" y1="12" x2="12" y2="16" />
|
||||
<line x1="12" y1="8" x2="12" y2="16" />
|
||||
<line x1="16" y1="12" x2="12" y2="16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="font-mono w-full leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
|
||||
bind:this={logsEl}
|
||||
on:scroll={detect}
|
||||
>
|
||||
<div class="px-2 pr-14">
|
||||
{#each logs as log}
|
||||
{log + '\n'}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
21
src/routes/services/[id]/umami/index.json.ts
Normal file
21
src/routes/services/[id]/umami/index.json.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { ErrorHandler } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
|
||||
let { name, fqdn } = await event.request.json();
|
||||
if (fqdn) fqdn = fqdn.toLowerCase();
|
||||
|
||||
try {
|
||||
await db.updateService({ id, fqdn, name });
|
||||
return { status: 201 };
|
||||
} catch (error) {
|
||||
return ErrorHandler(error);
|
||||
}
|
||||
};
|
||||
214
src/routes/services/[id]/umami/start.json.ts
Normal file
214
src/routes/services/[id]/umami/start.json.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import { asyncExecShell, createDirectories, getEngine, getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { promises as fs } from 'fs';
|
||||
import yaml from 'js-yaml';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { ErrorHandler, getServiceImage } from '$lib/database';
|
||||
import { makeLabelForServices } from '$lib/buildPacks/common';
|
||||
import type { ComposeFile } from '$lib/types/composeFile';
|
||||
import type { Service, DestinationDocker, Prisma } from '@prisma/client';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
|
||||
try {
|
||||
const service: Service & Prisma.ServiceInclude & { destinationDocker: DestinationDocker } =
|
||||
await db.getService({ id, teamId });
|
||||
const {
|
||||
type,
|
||||
version,
|
||||
destinationDockerId,
|
||||
destinationDocker,
|
||||
serviceSecret,
|
||||
umami: {
|
||||
umamiAdminPassword,
|
||||
postgresqlUser,
|
||||
postgresqlPassword,
|
||||
postgresqlDatabase,
|
||||
hashSalt
|
||||
}
|
||||
} = service;
|
||||
const network = destinationDockerId && destinationDocker.network;
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
const image = getServiceImage(type);
|
||||
|
||||
const config = {
|
||||
umami: {
|
||||
image: `${image}:${version}`,
|
||||
environmentVariables: {
|
||||
DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}`,
|
||||
DATABASE_TYPE: 'postgresql',
|
||||
HASH_SALT: hashSalt
|
||||
}
|
||||
},
|
||||
postgresql: {
|
||||
image: 'postgres:12-alpine',
|
||||
volume: `${id}-postgresql-data:/var/lib/postgresql/data`,
|
||||
environmentVariables: {
|
||||
POSTGRES_USER: postgresqlUser,
|
||||
POSTGRES_PASSWORD: postgresqlPassword,
|
||||
POSTGRES_DB: postgresqlDatabase
|
||||
}
|
||||
}
|
||||
};
|
||||
if (serviceSecret.length > 0) {
|
||||
serviceSecret.forEach((secret) => {
|
||||
config.umami.environmentVariables[secret.name] = secret.value;
|
||||
});
|
||||
}
|
||||
|
||||
const initDbSQL = `
|
||||
drop table if exists event;
|
||||
drop table if exists pageview;
|
||||
drop table if exists session;
|
||||
drop table if exists website;
|
||||
drop table if exists account;
|
||||
|
||||
create table account (
|
||||
user_id serial primary key,
|
||||
username varchar(255) unique not null,
|
||||
password varchar(60) not null,
|
||||
is_admin bool not null default false,
|
||||
created_at timestamp with time zone default current_timestamp,
|
||||
updated_at timestamp with time zone default current_timestamp
|
||||
);
|
||||
|
||||
create table website (
|
||||
website_id serial primary key,
|
||||
website_uuid uuid unique not null,
|
||||
user_id int not null references account(user_id) on delete cascade,
|
||||
name varchar(100) not null,
|
||||
domain varchar(500),
|
||||
share_id varchar(64) unique,
|
||||
created_at timestamp with time zone default current_timestamp
|
||||
);
|
||||
|
||||
create table session (
|
||||
session_id serial primary key,
|
||||
session_uuid uuid unique not null,
|
||||
website_id int not null references website(website_id) on delete cascade,
|
||||
created_at timestamp with time zone default current_timestamp,
|
||||
hostname varchar(100),
|
||||
browser varchar(20),
|
||||
os varchar(20),
|
||||
device varchar(20),
|
||||
screen varchar(11),
|
||||
language varchar(35),
|
||||
country char(2)
|
||||
);
|
||||
|
||||
create table pageview (
|
||||
view_id serial primary key,
|
||||
website_id int not null references website(website_id) on delete cascade,
|
||||
session_id int not null references session(session_id) on delete cascade,
|
||||
created_at timestamp with time zone default current_timestamp,
|
||||
url varchar(500) not null,
|
||||
referrer varchar(500)
|
||||
);
|
||||
|
||||
create table event (
|
||||
event_id serial primary key,
|
||||
website_id int not null references website(website_id) on delete cascade,
|
||||
session_id int not null references session(session_id) on delete cascade,
|
||||
created_at timestamp with time zone default current_timestamp,
|
||||
url varchar(500) not null,
|
||||
event_type varchar(50) not null,
|
||||
event_value varchar(50) not null
|
||||
);
|
||||
|
||||
create index website_user_id_idx on website(user_id);
|
||||
|
||||
create index session_created_at_idx on session(created_at);
|
||||
create index session_website_id_idx on session(website_id);
|
||||
|
||||
create index pageview_created_at_idx on pageview(created_at);
|
||||
create index pageview_website_id_idx on pageview(website_id);
|
||||
create index pageview_session_id_idx on pageview(session_id);
|
||||
create index pageview_website_id_created_at_idx on pageview(website_id, created_at);
|
||||
create index pageview_website_id_session_id_created_at_idx on pageview(website_id, session_id, created_at);
|
||||
|
||||
create index event_created_at_idx on event(created_at);
|
||||
create index event_website_id_idx on event(website_id);
|
||||
create index event_session_id_idx on event(session_id);
|
||||
|
||||
insert into account (username, password, is_admin) values ('admin', '${bcrypt.hashSync(
|
||||
umamiAdminPassword,
|
||||
10
|
||||
)}', true);`;
|
||||
await fs.writeFile(`${workdir}/schema.postgresql.sql`, initDbSQL);
|
||||
const Dockerfile = `
|
||||
FROM ${config.postgresql.image}
|
||||
COPY ./schema.postgresql.sql /docker-entrypoint-initdb.d/schema.postgresql.sql`;
|
||||
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile);
|
||||
const composeFile: ComposeFile = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[id]: {
|
||||
container_name: id,
|
||||
image: config.umami.image,
|
||||
environment: config.umami.environmentVariables,
|
||||
networks: [network],
|
||||
volumes: [],
|
||||
restart: 'always',
|
||||
labels: makeLabelForServices('umami'),
|
||||
deploy: {
|
||||
restart_policy: {
|
||||
condition: 'on-failure',
|
||||
delay: '5s',
|
||||
max_attempts: 3,
|
||||
window: '120s'
|
||||
}
|
||||
},
|
||||
depends_on: [`${id}-postgresql`]
|
||||
},
|
||||
[`${id}-postgresql`]: {
|
||||
build: workdir,
|
||||
container_name: `${id}-postgresql`,
|
||||
environment: config.postgresql.environmentVariables,
|
||||
networks: [network],
|
||||
volumes: [config.postgresql.volume],
|
||||
restart: 'always',
|
||||
deploy: {
|
||||
restart_policy: {
|
||||
condition: 'on-failure',
|
||||
delay: '5s',
|
||||
max_attempts: 3,
|
||||
window: '120s'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
[network]: {
|
||||
external: true
|
||||
}
|
||||
},
|
||||
volumes: {
|
||||
[config.postgresql.volume.split(':')[0]]: {
|
||||
name: config.postgresql.volume.split(':')[0]
|
||||
}
|
||||
}
|
||||
};
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
|
||||
try {
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
|
||||
return {
|
||||
status: 200
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return ErrorHandler(error);
|
||||
}
|
||||
} catch (error) {
|
||||
return ErrorHandler(error);
|
||||
}
|
||||
};
|
||||
42
src/routes/services/[id]/umami/stop.json.ts
Normal file
42
src/routes/services/[id]/umami/stop.json.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { getUserDetails, removeDestinationDocker } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { ErrorHandler } from '$lib/database';
|
||||
import { checkContainer, stopTcpHttpProxy } from '$lib/haproxy';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
|
||||
try {
|
||||
const service = await db.getService({ id, teamId });
|
||||
const { destinationDockerId, destinationDocker } = service;
|
||||
if (destinationDockerId) {
|
||||
const engine = destinationDocker.engine;
|
||||
|
||||
try {
|
||||
const found = await checkContainer(engine, id);
|
||||
if (found) {
|
||||
await removeDestinationDocker({ id, engine });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
try {
|
||||
const found = await checkContainer(engine, `${id}-postgresql`);
|
||||
if (found) {
|
||||
await removeDestinationDocker({ id: `${id}-postgresql`, engine });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: 200
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorHandler(error);
|
||||
}
|
||||
};
|
||||
@@ -15,6 +15,9 @@
|
||||
import MeiliSearch from '$lib/components/svg/services/MeiliSearch.svelte';
|
||||
import { session } from '$app/stores';
|
||||
import { getDomain } from '$lib/components/common';
|
||||
import Umami from '$lib/components/svg/services/Umami.svelte';
|
||||
import Hasura from '$lib/components/svg/services/Hasura.svelte';
|
||||
import Fider from '$lib/components/svg/services/Fider.svelte';
|
||||
|
||||
export let services;
|
||||
async function newService() {
|
||||
@@ -86,6 +89,12 @@
|
||||
<Ghost isAbsolute />
|
||||
{:else if service.type === 'meilisearch'}
|
||||
<MeiliSearch isAbsolute />
|
||||
{:else if service.type === 'umami'}
|
||||
<Umami isAbsolute />
|
||||
{:else if service.type === 'hasura'}
|
||||
<Hasura isAbsolute />
|
||||
{:else if service.type === 'fider'}
|
||||
<Fider isAbsolute />
|
||||
{/if}
|
||||
<div class="truncate text-center text-xl font-bold">
|
||||
{service.name}
|
||||
@@ -133,6 +142,12 @@
|
||||
<Ghost isAbsolute />
|
||||
{:else if service.type === 'meilisearch'}
|
||||
<MeiliSearch isAbsolute />
|
||||
{:else if service.type === 'umami'}
|
||||
<Umami isAbsolute />
|
||||
{:else if service.type === 'hasura'}
|
||||
<Hasura isAbsolute />
|
||||
{:else if service.type === 'fider'}
|
||||
<Fider isAbsolute />
|
||||
{/if}
|
||||
<div class="truncate text-center text-xl font-bold">
|
||||
{service.name}
|
||||
|
||||
@@ -64,10 +64,21 @@ export const post: RequestHandler = async (event) => {
|
||||
};
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { fqdn, isRegistrationEnabled, dualCerts, minPort, maxPort } = await event.request.json();
|
||||
const {
|
||||
fqdn,
|
||||
isRegistrationEnabled,
|
||||
dualCerts,
|
||||
minPort,
|
||||
maxPort,
|
||||
isAutoUpdateEnabled,
|
||||
isDNSCheckEnabled
|
||||
} = await event.request.json();
|
||||
try {
|
||||
const { id } = await db.listSettings();
|
||||
await db.prisma.setting.update({ where: { id }, data: { isRegistrationEnabled, dualCerts } });
|
||||
await db.prisma.setting.update({
|
||||
where: { id },
|
||||
data: { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled, isDNSCheckEnabled }
|
||||
});
|
||||
if (fqdn) {
|
||||
await db.prisma.setting.update({ where: { id }, data: { fqdn } });
|
||||
}
|
||||
|
||||
@@ -28,8 +28,6 @@
|
||||
import { session } from '$app/stores';
|
||||
|
||||
export let settings;
|
||||
import Cookies from 'js-cookie';
|
||||
import langs from '$lib/lang.json';
|
||||
import Setting from '$lib/components/Setting.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import { errorNotification } from '$lib/form';
|
||||
@@ -39,11 +37,12 @@
|
||||
import { getDomain } from '$lib/components/common';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { t } from '$lib/translations';
|
||||
|
||||
import Language from './_Language.svelte';
|
||||
import { features } from '$lib/store';
|
||||
|
||||
let isRegistrationEnabled = settings.isRegistrationEnabled;
|
||||
let dualCerts = settings.dualCerts;
|
||||
let isAutoUpdateEnabled = settings.isAutoUpdateEnabled;
|
||||
let isDNSCheckEnabled = settings.isDNSCheckEnabled;
|
||||
|
||||
let minPort = settings.minPort;
|
||||
let maxPort = settings.maxPort;
|
||||
@@ -76,7 +75,18 @@
|
||||
if (name === 'dualCerts') {
|
||||
dualCerts = !dualCerts;
|
||||
}
|
||||
await post(`/settings.json`, { isRegistrationEnabled, dualCerts });
|
||||
if (name === 'isAutoUpdateEnabled') {
|
||||
isAutoUpdateEnabled = !isAutoUpdateEnabled;
|
||||
}
|
||||
if (name === 'isDNSCheckEnabled') {
|
||||
isDNSCheckEnabled = !isDNSCheckEnabled;
|
||||
}
|
||||
await post(`/settings.json`, {
|
||||
isRegistrationEnabled,
|
||||
dualCerts,
|
||||
isAutoUpdateEnabled,
|
||||
isDNSCheckEnabled
|
||||
});
|
||||
return toast.push(t.get('application.settings_saved'));
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
@@ -174,13 +184,21 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
bind:setting={isDNSCheckEnabled}
|
||||
title={$t('setting.is_dns_check_enabled')}
|
||||
description={$t('setting.is_dns_check_enabled_explainer')}
|
||||
on:click={() => changeSettings('isDNSCheckEnabled')}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
dataTooltip={$t('setting.must_remove_domain_before_changing')}
|
||||
disabled={isFqdnSet}
|
||||
bind:setting={dualCerts}
|
||||
title={$t('application.ssl_www_and_non_www')}
|
||||
description={$t('services.generate_www_non_www_ssl')}
|
||||
description={$t('setting.generate_www_non_www_ssl')}
|
||||
on:click={() => !isFqdnSet && changeSettings('dualCerts')}
|
||||
/>
|
||||
</div>
|
||||
@@ -192,6 +210,16 @@
|
||||
on:click={() => changeSettings('isRegistrationEnabled')}
|
||||
/>
|
||||
</div>
|
||||
{#if browser && $features.beta}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
bind:setting={isAutoUpdateEnabled}
|
||||
title={$t('setting.auto_update_enabled')}
|
||||
description={$t('setting.auto_update_enabled_explainer')}
|
||||
on:click={() => changeSettings('isAutoUpdateEnabled')}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
<div class="flex space-x-1 pt-6 font-bold">
|
||||
|
||||
@@ -100,11 +100,15 @@ export const post: RequestHandler = async (event) => {
|
||||
type: 'webhook_commit'
|
||||
}
|
||||
});
|
||||
await buildQueue.add(buildId, {
|
||||
build_id: buildId,
|
||||
type: 'webhook_commit',
|
||||
...applicationFound
|
||||
});
|
||||
await buildQueue.add(
|
||||
buildId,
|
||||
{
|
||||
build_id: buildId,
|
||||
type: 'webhook_commit',
|
||||
...applicationFound
|
||||
},
|
||||
{ jobId: buildId }
|
||||
);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
@@ -160,13 +164,17 @@ export const post: RequestHandler = async (event) => {
|
||||
type: 'webhook_pr'
|
||||
}
|
||||
});
|
||||
await buildQueue.add(buildId, {
|
||||
build_id: buildId,
|
||||
type: 'webhook_pr',
|
||||
...applicationFound,
|
||||
sourceBranch,
|
||||
pullmergeRequestId
|
||||
});
|
||||
await buildQueue.add(
|
||||
buildId,
|
||||
{
|
||||
build_id: buildId,
|
||||
type: 'webhook_pr',
|
||||
...applicationFound,
|
||||
sourceBranch,
|
||||
pullmergeRequestId
|
||||
},
|
||||
{ jobId: buildId }
|
||||
);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
|
||||
@@ -73,11 +73,15 @@ export const post: RequestHandler = async (event) => {
|
||||
type: 'webhook_commit'
|
||||
}
|
||||
});
|
||||
await buildQueue.add(buildId, {
|
||||
build_id: buildId,
|
||||
type: 'webhook_commit',
|
||||
...applicationFound
|
||||
});
|
||||
await buildQueue.add(
|
||||
buildId,
|
||||
{
|
||||
build_id: buildId,
|
||||
type: 'webhook_commit',
|
||||
...applicationFound
|
||||
},
|
||||
{ jobId: buildId }
|
||||
);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
@@ -156,13 +160,17 @@ export const post: RequestHandler = async (event) => {
|
||||
type: 'webhook_mr'
|
||||
}
|
||||
});
|
||||
await buildQueue.add(buildId, {
|
||||
build_id: buildId,
|
||||
type: 'webhook_mr',
|
||||
...applicationFound,
|
||||
sourceBranch,
|
||||
pullmergeRequestId
|
||||
});
|
||||
await buildQueue.add(
|
||||
buildId,
|
||||
{
|
||||
build_id: buildId,
|
||||
type: 'webhook_mr',
|
||||
...applicationFound,
|
||||
sourceBranch,
|
||||
pullmergeRequestId
|
||||
},
|
||||
{ jobId: buildId }
|
||||
);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
|
||||
@@ -46,14 +46,14 @@ textarea {
|
||||
}
|
||||
|
||||
#svelte .custom-select-wrapper .selectContainer {
|
||||
@apply h-12 w-96 rounded border-none bg-coolgray-200 p-2 text-xs font-bold tracking-tight outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 md:text-sm;
|
||||
@apply h-12 w-96 rounded border-none bg-coolgray-200 p-2 px-0 text-xs tracking-tight outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 md:text-sm;
|
||||
}
|
||||
|
||||
#svelte .listContainer {
|
||||
@apply bg-coolgray-400 text-white scrollbar-w-2 scrollbar-thumb-green-500 scrollbar-track-coolgray-200;
|
||||
}
|
||||
#svelte .selectedItem {
|
||||
@apply pl-3;
|
||||
@apply pl-2;
|
||||
}
|
||||
|
||||
#svelte .item.hover {
|
||||
@@ -64,7 +64,7 @@ textarea {
|
||||
}
|
||||
|
||||
select {
|
||||
@apply h-12 w-96 rounded bg-coolgray-200 p-2 text-xs font-bold tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:text-stone-600 md:text-sm;
|
||||
@apply h-12 w-96 rounded bg-coolgray-200 p-2 text-xs font-bold tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:text-stone-600 md:text-sm;
|
||||
}
|
||||
.svelte-select {
|
||||
--background: rgb(32 32 32);
|
||||
|
||||
Reference in New Issue
Block a user