mirror of
https://github.com/ershisan99/coolify.git
synced 2025-12-26 12:33:25 +00:00
Compare commits
204 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
984fe01551 | ||
|
|
d0cb350687 | ||
|
|
5f51011ce1 | ||
|
|
9ca125ac55 | ||
|
|
360f4f8c27 | ||
|
|
6501f71bd6 | ||
|
|
bf6b799dba | ||
|
|
5f57279283 | ||
|
|
5ed3565520 | ||
|
|
513fa90b8a | ||
|
|
a4d9b9689b | ||
|
|
1c05c0dcbb | ||
|
|
a1b49a3a6b | ||
|
|
6f57298cbb | ||
|
|
d8ce673088 | ||
|
|
4cd7af7a74 | ||
|
|
49c61b5992 | ||
|
|
e44d0550d2 | ||
|
|
17f82109b6 | ||
|
|
2d8888ae9b | ||
|
|
4abe9c6fb2 | ||
|
|
f9d94fa660 | ||
|
|
eaa13f4990 | ||
|
|
01fd5901fe | ||
|
|
3d6adeffc4 | ||
|
|
9066952759 | ||
|
|
6dd7f6274a | ||
|
|
7a8fe6d152 | ||
|
|
be507be3a9 | ||
|
|
657b97f190 | ||
|
|
9d7745cd9b | ||
|
|
3668f83693 | ||
|
|
a2d5d99c1f | ||
|
|
f379ef6a3b | ||
|
|
510a748749 | ||
|
|
550150d685 | ||
|
|
011ea9659e | ||
|
|
6eca7d948e | ||
|
|
90e639f119 | ||
|
|
86ac6461d1 | ||
|
|
18a95bf9ab | ||
|
|
7949bbe66d | ||
|
|
4b603c452a | ||
|
|
837f0634b6 | ||
|
|
78076f7854 | ||
|
|
719350cee1 | ||
|
|
4f6be3e6f5 | ||
|
|
8e61e9fecb | ||
|
|
2083285d78 | ||
|
|
034e86e2cb | ||
|
|
f4a2d5c652 | ||
|
|
534ccd6bf6 | ||
|
|
c17064f853 | ||
|
|
1e1566082f | ||
|
|
449548654d | ||
|
|
6fc99524f0 | ||
|
|
051629fad3 | ||
|
|
f957008c1c | ||
|
|
98e1deec88 | ||
|
|
99127652af | ||
|
|
e9b9e9e82c | ||
|
|
2ed5c3746e | ||
|
|
8902056fdb | ||
|
|
defa6ff6e8 | ||
|
|
eed44e81be | ||
|
|
1951aec5ec | ||
|
|
9c4e0b4107 | ||
|
|
c8deac660d | ||
|
|
4cc5ec9bd0 | ||
|
|
c41bef2e81 | ||
|
|
5b735cf960 | ||
|
|
604e960aa9 | ||
|
|
6c465aa1f2 | ||
|
|
c266832fdc | ||
|
|
906d8d0413 | ||
|
|
cb05fd4a3c | ||
|
|
2eda24799b | ||
|
|
41e221f0cb | ||
|
|
f75af035bb | ||
|
|
e9e6449edf | ||
|
|
f09d76da35 | ||
|
|
40dfe0919b | ||
|
|
85990dd074 | ||
|
|
38acc16e1c | ||
|
|
b7cc4c1e92 | ||
|
|
1f232d96d8 | ||
|
|
83508f165d | ||
|
|
7cc58e7e84 | ||
|
|
31d9740aac | ||
|
|
69891a64a0 | ||
|
|
0940309600 | ||
|
|
a762b1ed60 | ||
|
|
1b9d9d3a8b | ||
|
|
d9908b3d61 | ||
|
|
c40b80436a | ||
|
|
8f1e352bcc | ||
|
|
18e769b5e5 | ||
|
|
27af6459b3 | ||
|
|
2c4bfab01a | ||
|
|
e689be552b | ||
|
|
ad80e7f48b | ||
|
|
d81b75b084 | ||
|
|
90f1431047 | ||
|
|
61ea7dabae | ||
|
|
5d9f5f4a7d | ||
|
|
f956f612d3 | ||
|
|
3f5108268d | ||
|
|
4c0dfc3f30 | ||
|
|
1670fe9b1c | ||
|
|
300b28c0f2 | ||
|
|
e7038961ef | ||
|
|
24e77a5211 | ||
|
|
9df039fbc2 | ||
|
|
143cd46a81 | ||
|
|
680e9871ed | ||
|
|
d5ece58f71 | ||
|
|
d7bbb5c4b7 | ||
|
|
cf9c991c79 | ||
|
|
0f0d96195d | ||
|
|
3a562bb714 | ||
|
|
6381ba8478 | ||
|
|
9e3c14841a | ||
|
|
1917091338 | ||
|
|
b1bb508554 | ||
|
|
0a68a48fc5 | ||
|
|
d3af6792d0 | ||
|
|
44dc3b743e | ||
|
|
b469d2832d | ||
|
|
d844026c29 | ||
|
|
21b4990652 | ||
|
|
39e24bdc97 | ||
|
|
bc66b98176 | ||
|
|
d6d3fb46cc | ||
|
|
4040b334f5 | ||
|
|
d7e72519ef | ||
|
|
c7752f0be9 | ||
|
|
0ffe28a733 | ||
|
|
56f24fe317 | ||
|
|
341cde2781 | ||
|
|
33bb8d434d | ||
|
|
9f813b7385 | ||
|
|
02a336a25d | ||
|
|
88ed1446f4 | ||
|
|
c69312f128 | ||
|
|
c5bcff0e10 | ||
|
|
871d1e2440 | ||
|
|
1619afb938 | ||
|
|
25528913f1 | ||
|
|
7df532fa72 | ||
|
|
ef91441c76 | ||
|
|
aa6c56b63d | ||
|
|
18e899d15e | ||
|
|
63fa8924ae | ||
|
|
0e13e3bd81 | ||
|
|
372c0ed457 | ||
|
|
071077200b | ||
|
|
65579a2861 | ||
|
|
bb7603ae2a | ||
|
|
cce67d274e | ||
|
|
794329dcad | ||
|
|
e36fda3ff1 | ||
|
|
3832d33259 | ||
|
|
1f40c2ccf8 | ||
|
|
7350524456 | ||
|
|
a1a973a873 | ||
|
|
f2a915700c | ||
|
|
e184f99617 | ||
|
|
ab07adb14f | ||
|
|
6535c68276 | ||
|
|
dde2772e52 | ||
|
|
4a8fd309c5 | ||
|
|
b416849d9c | ||
|
|
bc321d8ced | ||
|
|
ac72c19d22 | ||
|
|
67fc2fd3c0 | ||
|
|
4acc59204c | ||
|
|
07cadb59e0 | ||
|
|
6fa4741c81 | ||
|
|
f4bac2382c | ||
|
|
67b72220c0 | ||
|
|
feeb14ea47 | ||
|
|
bdfb6f5f46 | ||
|
|
53491e9eaa | ||
|
|
f720de65fa | ||
|
|
3d70162a8d | ||
|
|
45919fc0cf | ||
|
|
dd6f4c4844 | ||
|
|
bb47db033f | ||
|
|
111ea78693 | ||
|
|
c17253589a | ||
|
|
7e6156f5dd | ||
|
|
d5cfb63f52 | ||
|
|
cab15055e7 | ||
|
|
9185910171 | ||
|
|
b4892e0caf | ||
|
|
83e0cafef9 | ||
|
|
7cb75506c3 | ||
|
|
ac6970ad40 | ||
|
|
5a95cc236c | ||
|
|
95c942f477 | ||
|
|
7ed1ced521 | ||
|
|
801b9c1483 | ||
|
|
3990bebca3 | ||
|
|
8a2de1001f |
@@ -26,7 +26,7 @@
|
|||||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
"forwardPorts": [3000, 3001],
|
"forwardPorts": [3000, 3001],
|
||||||
// Use 'postCreateCommand' to run commands after the container is created.
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
"postCreateCommand": "cp apps/api/.env.example pps/api/.env && pnpm install && pnpm db:push && pnpm db:seed",
|
"postCreateCommand": "cp apps/api/.env.example apps/api/.env && pnpm install && pnpm db:push && pnpm db:seed",
|
||||||
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||||
"remoteUser": "node",
|
"remoteUser": "node",
|
||||||
"features": {
|
"features": {
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -12,4 +12,6 @@ client
|
|||||||
apps/api/db/*.db
|
apps/api/db/*.db
|
||||||
local-serve
|
local-serve
|
||||||
apps/api/db/migration.db-journal
|
apps/api/db/migration.db-journal
|
||||||
apps/api/core*
|
apps/api/core*
|
||||||
|
logs
|
||||||
|
others/certificates
|
||||||
256
CONTRIBUTING.md
256
CONTRIBUTING.md
@@ -1,256 +0,0 @@
|
|||||||
# 👋 Welcome
|
|
||||||
|
|
||||||
First of all, thank you for considering contributing to my project! It means a lot 💜.
|
|
||||||
|
|
||||||
|
|
||||||
## 🙋 Want to help?
|
|
||||||
|
|
||||||
If you begin in GitHub contribution, you can find the [first contribution](https://github.com/firstcontributions/first-contributions) and follow this guide.
|
|
||||||
|
|
||||||
Follow the [introduction](#introduction) to get started then start contributing!
|
|
||||||
|
|
||||||
This is a little list of what you can do to help the project:
|
|
||||||
|
|
||||||
- [🧑💻 Develop your own ideas](#developer-contribution)
|
|
||||||
- [🌐 Translate the project](#translation)
|
|
||||||
|
|
||||||
## 👋 Introduction
|
|
||||||
|
|
||||||
### Setup with Github codespaces
|
|
||||||
|
|
||||||
If you have github codespaces enabled then you can just create a codespace and run `pnpm dev` to run your the dev environment. All the required dependencies and packages has been configured for you already.
|
|
||||||
|
|
||||||
### Setup with Gitpod
|
|
||||||
|
|
||||||
If you have a [Gitpod](https://gitpod.io), you can just create a workspace from this repository, run `pnpm install && pnpm db:push && pnpm db:seed` and then `pnpm dev`. All the required dependencies and packages has been configured for you already.
|
|
||||||
|
|
||||||
### Setup locally in your machine
|
|
||||||
|
|
||||||
> 🔴 At the moment, Coolify **doesn't support Windows**. You must use Linux or MacOS. Consider using Gitpod or Github Codespaces.
|
|
||||||
|
|
||||||
#### Recommended Pull Request Guideline
|
|
||||||
|
|
||||||
- Fork the project
|
|
||||||
- Clone your fork repo to local
|
|
||||||
- Create a new branch
|
|
||||||
- Push to your fork repo
|
|
||||||
- Create a pull request: https://github.com/coollabsio/coolify/compare
|
|
||||||
- Write a proper description
|
|
||||||
- Open the pull request to review against `next` branch
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🧑💻 Developer contribution
|
|
||||||
## Technical skills required
|
|
||||||
|
|
||||||
- **Languages**: Node.js / Javascript / Typescript
|
|
||||||
- **Framework JS/TS**: [SvelteKit](https://kit.svelte.dev/) & [Fastify](https://www.fastify.io/)
|
|
||||||
- **Database ORM**: [Prisma.io](https://www.prisma.io/)
|
|
||||||
- **Docker Engine API**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## How to start after you set up your local fork?
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
1. Due to the lock file, this repository is best with [pnpm](https://pnpm.io). I recommend you try and use `pnpm` because it is cool and efficient!
|
|
||||||
|
|
||||||
2. You need to have [Docker Engine](https://docs.docker.com/engine/install/) installed locally.
|
|
||||||
3. You need to have [Docker Compose Plugin](https://docs.docker.com/compose/install/compose-plugin/) installed locally.
|
|
||||||
4. You need to have [GIT LFS Support](https://git-lfs.github.com/) installed locally.
|
|
||||||
|
|
||||||
Optional:
|
|
||||||
|
|
||||||
4. To test Heroku buildpacks, you need [pack](https://github.com/buildpacks/pack) binary installed locally.
|
|
||||||
|
|
||||||
### Steps for local setup
|
|
||||||
|
|
||||||
1. Copy `apps/api/.env.template` to `apps/api/.env.template` and set the `COOLIFY_APP_ID` environment variable to something cool.
|
|
||||||
2. Install dependencies with `pnpm install`.
|
|
||||||
3. Need to create a local SQlite database with `pnpm db:push`.
|
|
||||||
|
|
||||||
This will apply all migrations at `db/dev.db`.
|
|
||||||
|
|
||||||
4. Seed the database with base entities with `pnpm db:seed`
|
|
||||||
5. You can start coding after starting `pnpm dev`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Database migrations
|
|
||||||
|
|
||||||
During development, if you change the database layout, you need to run `pnpm db:push` to migrate the database and create types for Prisma. You also need to restart the development process.
|
|
||||||
|
|
||||||
If the schema is finalized, you need to create a migration file with `pnpm db:migrate <nameOfMigration>` where `nameOfMigration` is given by you. Make it sense. :)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## How to add new services
|
|
||||||
|
|
||||||
You can add any open-source and self-hostable software (service/application) to Coolify if the following statements are true:
|
|
||||||
|
|
||||||
- Self-hostable (obviously)
|
|
||||||
- Open-source
|
|
||||||
- Maintained (I do not want to add software full of bugs)
|
|
||||||
|
|
||||||
## Backend
|
|
||||||
|
|
||||||
There are 5 steps you should make on the backend side.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
> I will use [Umami](https://umami.is/) as an example service.
|
|
||||||
|
|
||||||
### Create Prisma / Database schema for the new 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.
|
|
||||||
|
|
||||||
Update Prisma schema in [prisma/schema.prisma](prisma/schema.prisma).
|
|
||||||
|
|
||||||
- Add new model with the new service name.
|
|
||||||
- Make a relationship 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.
|
|
||||||
|
|
||||||
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/TLS, you probably need to restart the `Typescript Language Server` to get the new types loaded in the running environment.
|
|
||||||
|
|
||||||
### Add supported versions
|
|
||||||
|
|
||||||
Supported versions are hardcoded into Coolify (for now).
|
|
||||||
|
|
||||||
You need to update `supportedServiceTypesAndVersions` function at [apps/api/src/lib/services/supportedVersions.ts](apps/api/src/lib/services/supportedVersions.ts). Example JSON:
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
// Name used to identify the service internally
|
|
||||||
name: 'umami',
|
|
||||||
// Fancier name to show to the user
|
|
||||||
fancyName: 'Umami',
|
|
||||||
// Docker base image for the service
|
|
||||||
baseImage: 'ghcr.io/mikecao/umami',
|
|
||||||
// Optional: If there is any dependent image, you should list it here
|
|
||||||
images: [],
|
|
||||||
// Usable tags
|
|
||||||
versions: ['postgresql-latest'],
|
|
||||||
// Which tag is the recommended
|
|
||||||
recommendedVersion: 'postgresql-latest',
|
|
||||||
// Application's default port, Umami listens on 3000
|
|
||||||
ports: {
|
|
||||||
main: 3000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Add required functions/properties
|
|
||||||
|
|
||||||
1. Add the new service to the `includeServices` variable in [apps/api/src/lib/services/common.ts](apps/api/src/lib/services/common.ts), so it will be included in all places in the database queries where it is required.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const include: any = {
|
|
||||||
destinationDocker: true,
|
|
||||||
persistentStorage: true,
|
|
||||||
serviceSecret: true,
|
|
||||||
minio: true,
|
|
||||||
plausibleAnalytics: true,
|
|
||||||
vscodeserver: true,
|
|
||||||
wordpress: true,
|
|
||||||
ghost: true,
|
|
||||||
meiliSearch: true,
|
|
||||||
umami: true // This line!
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Update the database update query with the new service type to `configureServiceType` function in [apps/api/src/lib/services/common.ts](apps/api/src/lib/services/common.ts). This function defines the automatically generated variables (passwords, users, etc.) and it's encryption process (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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Add field details to [apps/api/src/lib/services/serviceFields.ts](apps/api/src/lib/services/serviceFields.ts), so every component will know what to do with the values (decrypt/show it by default/readonly)
|
|
||||||
|
|
||||||
```js
|
|
||||||
export const umami = [{
|
|
||||||
name: 'postgresqlUser',
|
|
||||||
isEditable: false,
|
|
||||||
isLowerCase: false,
|
|
||||||
isNumber: false,
|
|
||||||
isBoolean: false,
|
|
||||||
isEncrypted: false
|
|
||||||
}]
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Add service deletion query to `removeService` function in [apps/api/src/lib/services/common.ts](apps/api/src/lib/services/common.ts)
|
|
||||||
|
|
||||||
5. Add start process for the new service in [apps/api/src/lib/services/handlers.ts](apps/api/src/lib/services/handlers.ts)
|
|
||||||
|
|
||||||
> See startUmamiService() function as example.
|
|
||||||
|
|
||||||
6. Add the newly added start process to `startService` in [apps/api/src/routes/api/v1/services/handlers.ts](apps/api/src/routes/api/v1/services/handlers.ts)
|
|
||||||
|
|
||||||
7. You need to add a custom logo at [apps/ui/src/lib/components/svg/services](apps/ui/src/lib/components/svg/services) as a svelte component and export it in [apps/ui/src/lib/components/svg/services/index.ts](apps/ui/src/lib/components/svg/services/index.ts)
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
8. You need to include it the logo at:
|
|
||||||
|
|
||||||
- [apps/ui/src/lib/components/svg/services/ServiceIcons.svelte](apps/ui/src/lib/components/svg/services/ServiceIcons.svelte) with `isAbsolute`.
|
|
||||||
- [apps/ui/src/routes/services/[id]/_ServiceLinks.svelte](apps/ui/src/routes/services/[id]/_ServiceLinks.svelte) with the link to the docs/main site of the service
|
|
||||||
|
|
||||||
9. By default the URL and the name frontend forms are included in [apps/ui/src/routes/services/[id]/_Services/_Services.svelte](apps/ui/src/routes/services/[id]/_Services/_Services.svelte).
|
|
||||||
|
|
||||||
If you need to show more details on the frontend, such as users/passwords, you need to add Svelte component to [apps/ui/src/routes/services/[id]/_Services](apps/ui/src/routes/services/[id]/_Services) with an underscore.
|
|
||||||
|
|
||||||
> For example, see other [here](apps/ui/src/routes/services/[id]/_Services/_Umami.svelte).
|
|
||||||
|
|
||||||
|
|
||||||
Good job! 👏
|
|
||||||
|
|
||||||
<!-- # 🌐 Translate the project
|
|
||||||
|
|
||||||
The project use [sveltekit-i18n](https://github.com/sveltekit-i18n/lib) to translate the project.
|
|
||||||
It follows the [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) to name languages.
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
You must have gone throw all the [intro](#introduction) steps before you can start translating.
|
|
||||||
|
|
||||||
It's only an advice, but I recommend you to use:
|
|
||||||
|
|
||||||
- Visual Studio Code
|
|
||||||
- [i18n Ally for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=Lokalise.i18n-ally): ideal to see the progress of the translation.
|
|
||||||
- [Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode): to get the syntax color for the project
|
|
||||||
|
|
||||||
### Adding a language
|
|
||||||
|
|
||||||
If your language doesn't appear in the [locales folder list](src/lib/locales/), follow the step below:
|
|
||||||
|
|
||||||
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! -->
|
|
||||||
@@ -1,45 +1,3 @@
|
|||||||
---
|
|
||||||
head:
|
|
||||||
- - meta
|
|
||||||
- name: description
|
|
||||||
content: Coolify - Databases
|
|
||||||
- - meta
|
|
||||||
- name: keywords
|
|
||||||
content: databases coollabs coolify
|
|
||||||
- - meta
|
|
||||||
- name: twitter:card
|
|
||||||
content: summary_large_image
|
|
||||||
- - meta
|
|
||||||
- name: twitter:site
|
|
||||||
content: '@andrasbacsai'
|
|
||||||
- - meta
|
|
||||||
- name: twitter:title
|
|
||||||
content: Coolify
|
|
||||||
- - meta
|
|
||||||
- name: twitter:description
|
|
||||||
content: An open-source & self-hostable Heroku / Netlify alternative.
|
|
||||||
- - meta
|
|
||||||
- name: twitter:image
|
|
||||||
content: https://cdn.coollabs.io/assets/coollabs/og-image-databases.png
|
|
||||||
- - meta
|
|
||||||
- property: og:type
|
|
||||||
content: website
|
|
||||||
- - meta
|
|
||||||
- property: og:url
|
|
||||||
content: https://coolify.io
|
|
||||||
- - meta
|
|
||||||
- property: og:title
|
|
||||||
content: Coolify
|
|
||||||
- - meta
|
|
||||||
- property: og:description
|
|
||||||
content: An open-source & self-hostable Heroku / Netlify alternative.
|
|
||||||
- - meta
|
|
||||||
- property: og:site_name
|
|
||||||
content: Coolify
|
|
||||||
- - meta
|
|
||||||
- property: og:image
|
|
||||||
content: https://cdn.coollabs.io/assets/coollabs/og-image-databases.png
|
|
||||||
---
|
|
||||||
# Contribution
|
# Contribution
|
||||||
|
|
||||||
First, thanks for considering to contribute to my project. It really means a lot! :)
|
First, thanks for considering to contribute to my project. It really means a lot! :)
|
||||||
@@ -100,9 +58,58 @@ All data that needs to be persist for a service should be saved to the database
|
|||||||
|
|
||||||
very password/api key/passphrase needs to be encrypted. If you are not sure, whether it should be encrypted or not, just encrypt it.
|
very password/api key/passphrase needs to be encrypted. If you are not sure, whether it should be encrypted or not, just encrypt it.
|
||||||
|
|
||||||
Update Prisma schema in [src/api/prisma/schema.prisma](https://github.com/coollabsio/coolify/blob/main/apps/api/prisma/schema.prisma).
|
Update Prisma schema in [src/apps/api/prisma/schema.prisma](https://github.com/coollabsio/coolify/blob/main/apps/api/prisma/schema.prisma).
|
||||||
|
|
||||||
- Add new model with the new service name.
|
- Add new model with the new service name.
|
||||||
- Make a relationship with `Service` model.
|
- Make a relationship with `Service` model.
|
||||||
- In the `Service` model, the name of the new field should be with low-capital.
|
- 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.
|
- 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.
|
||||||
|
|
||||||
|
Once done, create Prisma schema with `pnpm db:push`.
|
||||||
|
> You may also need to restart `Typescript Language Server` in your IDE to get the new types.
|
||||||
|
|
||||||
|
### Add available versions
|
||||||
|
|
||||||
|
Versions are hardcoded into Coolify at the moment and based on Docker image tags.
|
||||||
|
- Update `supportedServiceTypesAndVersions` function [here](apps/api/src/lib/services/supportedVersions.ts)
|
||||||
|
|
||||||
|
### Include the new service in queries
|
||||||
|
|
||||||
|
At [here](apps/api/src/lib/services/common.ts) in `includeServices` function add the new table name, so it will be included in all places in the database queries where it is required.
|
||||||
|
|
||||||
|
### Define auto-generated fields
|
||||||
|
|
||||||
|
At [here](apps/api/src/lib/services/common.ts) in `configureServiceType` function add the initial auto-generated details such as password, users etc, and the encryption process of secrets (if applicable).
|
||||||
|
|
||||||
|
### Define input field details
|
||||||
|
|
||||||
|
At [here](apps/api/src/lib/services/serviceFields.ts) add details about the input fields shown in the UI, so every component (API/UI) will know what to do with the values (decrypt/show it by default/readonly/etc).
|
||||||
|
|
||||||
|
### Define the start process
|
||||||
|
|
||||||
|
- At [here](apps/api/src/lib/services/handlers.ts), define how the service should start. It could be complex and based on `docker-compose` definitions.
|
||||||
|
|
||||||
|
> See `startUmamiService()` function as example.
|
||||||
|
|
||||||
|
- At [here](apps/api/src/routes/api/v1/services/handlers.ts), add the new start service process to `startService` function.
|
||||||
|
|
||||||
|
### Define the deletion process
|
||||||
|
|
||||||
|
[Here](apps/api/src/lib/services/common.ts) in `removeService` add the database deletion process.
|
||||||
|
|
||||||
|
### Custom logo
|
||||||
|
|
||||||
|
- At [here](apps/ui/src/lib/components/svg/services) add the service custom log as a Svelte component and export it [here](apps/ui/src/lib/components/svg/services/index.ts).
|
||||||
|
|
||||||
|
> 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.
|
||||||
|
|
||||||
|
- At [here](apps/ui/src/lib/components/svg/services/ServiceIcons.svelte) include the new logo with `isAbsolute` property.
|
||||||
|
|
||||||
|
- At [here](apps/ui/src/routes/services/[id]/_ServiceLinks.svelte) add links to the documentation of the service.
|
||||||
|
|
||||||
|
### Custom fields on the UI
|
||||||
|
By default the URL and name are shown on the UI. Everything else needs to be added [here](apps/ui/src/routes/services/[id]/_Services/_Services.svelte)
|
||||||
|
|
||||||
|
> If you need to show more details on the frontend, such as users/passwords, you need to add Svelte component [here](apps/ui/src/routes/services/[id]/_Services) with an underscore. For example, see other [here](apps/ui/src/routes/services/[id]/_Services/_Umami.svelte).
|
||||||
|
|
||||||
|
Good job! 👏
|
||||||
@@ -33,6 +33,7 @@ RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker
|
|||||||
RUN (curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.27.0/pack-v0.27.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack)
|
RUN (curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.27.0/pack-v0.27.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack)
|
||||||
|
|
||||||
COPY --from=build /app/apps/api/build/ .
|
COPY --from=build /app/apps/api/build/ .
|
||||||
|
COPY --from=build /app/others/fluentbit/ ./fluentbit
|
||||||
COPY --from=build /app/apps/ui/build/ ./public
|
COPY --from=build /app/apps/ui/build/ ./public
|
||||||
COPY --from=build /app/apps/api/prisma/ ./prisma
|
COPY --from=build /app/apps/api/prisma/ ./prisma
|
||||||
COPY --from=build /app/apps/api/package.json .
|
COPY --from=build /app/apps/api/package.json .
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"@fastify/cors": "8.1.0",
|
"@fastify/cors": "8.1.0",
|
||||||
"@fastify/env": "4.1.0",
|
"@fastify/env": "4.1.0",
|
||||||
"@fastify/jwt": "6.3.2",
|
"@fastify/jwt": "6.3.2",
|
||||||
|
"@fastify/multipart": "7.2.0",
|
||||||
"@fastify/static": "6.5.0",
|
"@fastify/static": "6.5.0",
|
||||||
"@iarna/toml": "2.2.5",
|
"@iarna/toml": "2.2.5",
|
||||||
"@ladjs/graceful": "3.0.2",
|
"@ladjs/graceful": "3.0.2",
|
||||||
@@ -29,6 +30,8 @@
|
|||||||
"bree": "9.1.2",
|
"bree": "9.1.2",
|
||||||
"cabin": "9.1.2",
|
"cabin": "9.1.2",
|
||||||
"compare-versions": "5.0.1",
|
"compare-versions": "5.0.1",
|
||||||
|
"csv-parse": "^5.3.0",
|
||||||
|
"csvtojson": "^2.0.10",
|
||||||
"cuid": "2.1.8",
|
"cuid": "2.1.8",
|
||||||
"dayjs": "1.11.5",
|
"dayjs": "1.11.5",
|
||||||
"dockerode": "3.3.4",
|
"dockerode": "3.3.4",
|
||||||
@@ -47,6 +50,7 @@
|
|||||||
"p-all": "4.0.0",
|
"p-all": "4.0.0",
|
||||||
"p-throttle": "5.0.0",
|
"p-throttle": "5.0.0",
|
||||||
"public-ip": "6.0.1",
|
"public-ip": "6.0.1",
|
||||||
|
"pump": "^3.0.0",
|
||||||
"ssh-config": "4.1.6",
|
"ssh-config": "4.1.6",
|
||||||
"strip-ansi": "7.0.1",
|
"strip-ansi": "7.0.1",
|
||||||
"unique-names-generator": "4.7.1"
|
"unique-names-generator": "4.7.1"
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "DatabaseSecret" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"value" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
"databaseId" TEXT NOT NULL,
|
||||||
|
CONSTRAINT "DatabaseSecret_databaseId_fkey" FOREIGN KEY ("databaseId") REFERENCES "Database" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "DatabaseSecret_name_databaseId_key" ON "DatabaseSecret"("name", "databaseId");
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Build" ADD COLUMN "previewApplicationId" TEXT;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "PreviewApplication" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"pullmergeRequestId" TEXT NOT NULL,
|
||||||
|
"sourceBranch" TEXT NOT NULL,
|
||||||
|
"isRandomDomain" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"customDomain" TEXT,
|
||||||
|
"applicationId" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "PreviewApplication_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "PreviewApplication_applicationId_key" ON "PreviewApplication"("applicationId");
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Certificate" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"key" TEXT NOT NULL,
|
||||||
|
"cert" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
"teamId" TEXT,
|
||||||
|
CONSTRAINT "Certificate_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
);
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
-- RedefineTables
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_ApplicationSettings" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"applicationId" TEXT NOT NULL,
|
||||||
|
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"debug" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"previews" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"autodeploy" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"isBot" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"isPublicRepository" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"isDBBranching" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"isCustomSSL" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "ApplicationSettings_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO "new_ApplicationSettings" ("applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isDBBranching", "isPublicRepository", "previews", "updatedAt") SELECT "applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isDBBranching", "isPublicRepository", "previews", "updatedAt" FROM "ApplicationSettings";
|
||||||
|
DROP TABLE "ApplicationSettings";
|
||||||
|
ALTER TABLE "new_ApplicationSettings" RENAME TO "ApplicationSettings";
|
||||||
|
CREATE UNIQUE INDEX "ApplicationSettings_applicationId_key" ON "ApplicationSettings"("applicationId");
|
||||||
|
PRAGMA foreign_key_check;
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
@@ -8,6 +8,16 @@ datasource db {
|
|||||||
url = env("COOLIFY_DATABASE_URL")
|
url = env("COOLIFY_DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Certificate {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
key String
|
||||||
|
cert String
|
||||||
|
team Team? @relation(fields: [teamId], references: [id])
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
teamId String?
|
||||||
|
}
|
||||||
|
|
||||||
model Setting {
|
model Setting {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
fqdn String? @unique
|
fqdn String? @unique
|
||||||
@@ -70,6 +80,7 @@ model Team {
|
|||||||
gitLabApps GitlabApp[]
|
gitLabApps GitlabApp[]
|
||||||
service Service[]
|
service Service[]
|
||||||
users User[]
|
users User[]
|
||||||
|
certificate Certificate[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model TeamInvitation {
|
model TeamInvitation {
|
||||||
@@ -119,6 +130,19 @@ model Application {
|
|||||||
secrets Secret[]
|
secrets Secret[]
|
||||||
teams Team[]
|
teams Team[]
|
||||||
connectedDatabase ApplicationConnectedDatabase?
|
connectedDatabase ApplicationConnectedDatabase?
|
||||||
|
previewApplication PreviewApplication[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model PreviewApplication {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
pullmergeRequestId String
|
||||||
|
sourceBranch String
|
||||||
|
isRandomDomain Boolean @default(false)
|
||||||
|
customDomain String?
|
||||||
|
applicationId String @unique
|
||||||
|
application Application @relation(fields: [applicationId], references: [id])
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
model ApplicationConnectedDatabase {
|
model ApplicationConnectedDatabase {
|
||||||
@@ -148,6 +172,7 @@ model ApplicationSettings {
|
|||||||
isBot Boolean @default(false)
|
isBot Boolean @default(false)
|
||||||
isPublicRepository Boolean @default(false)
|
isPublicRepository Boolean @default(false)
|
||||||
isDBBranching Boolean @default(false)
|
isDBBranching Boolean @default(false)
|
||||||
|
isCustomSSL Boolean @default(false)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
application Application @relation(fields: [applicationId], references: [id])
|
application Application @relation(fields: [applicationId], references: [id])
|
||||||
@@ -210,21 +235,22 @@ model BuildLog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Build {
|
model Build {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
type String
|
type String
|
||||||
applicationId String?
|
applicationId String?
|
||||||
destinationDockerId String?
|
destinationDockerId String?
|
||||||
gitSourceId String?
|
gitSourceId String?
|
||||||
githubAppId String?
|
githubAppId String?
|
||||||
gitlabAppId String?
|
gitlabAppId String?
|
||||||
commit String?
|
commit String?
|
||||||
pullmergeRequestId String?
|
pullmergeRequestId String?
|
||||||
forceRebuild Boolean @default(false)
|
previewApplicationId String?
|
||||||
sourceBranch String?
|
forceRebuild Boolean @default(false)
|
||||||
branch String?
|
sourceBranch String?
|
||||||
status String? @default("queued")
|
branch String?
|
||||||
createdAt DateTime @default(now())
|
status String? @default("queued")
|
||||||
updatedAt DateTime @updatedAt
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
model DestinationDocker {
|
model DestinationDocker {
|
||||||
@@ -328,6 +354,19 @@ model Database {
|
|||||||
settings DatabaseSettings?
|
settings DatabaseSettings?
|
||||||
teams Team[]
|
teams Team[]
|
||||||
applicationConnectedDatabase ApplicationConnectedDatabase[]
|
applicationConnectedDatabase ApplicationConnectedDatabase[]
|
||||||
|
databaseSecret DatabaseSecret[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model DatabaseSecret {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String
|
||||||
|
value String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
databaseId String
|
||||||
|
database Database @relation(fields: [databaseId], references: [id])
|
||||||
|
|
||||||
|
@@unique([name, databaseId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model DatabaseSettings {
|
model DatabaseSettings {
|
||||||
|
|||||||
@@ -94,6 +94,16 @@ async function main() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Set new preview secrets
|
||||||
|
const secrets = await prisma.secret.findMany({ where: { isPRMRSecret: false } })
|
||||||
|
if (secrets.length > 0) {
|
||||||
|
for (const secret of secrets) {
|
||||||
|
const previewSecrets = await prisma.secret.findMany({ where: { id: secret.id, isPRMRSecret: true } })
|
||||||
|
if (previewSecrets.length === 0) {
|
||||||
|
await prisma.secret.create({ data: { ...secret, id: undefined, isPRMRSecret: true } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
main()
|
main()
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import cors from '@fastify/cors';
|
|||||||
import serve from '@fastify/static';
|
import serve from '@fastify/static';
|
||||||
import env from '@fastify/env';
|
import env from '@fastify/env';
|
||||||
import cookie from '@fastify/cookie';
|
import cookie from '@fastify/cookie';
|
||||||
|
import multipart from '@fastify/multipart';
|
||||||
import path, { join } from 'path';
|
import path, { join } from 'path';
|
||||||
import autoLoad from '@fastify/autoload';
|
import autoLoad from '@fastify/autoload';
|
||||||
import { asyncExecShell, createRemoteEngineConfiguration, getDomain, isDev, listSettings, prisma, version } from './lib/common';
|
import { asyncExecShell, createRemoteEngineConfiguration, getDomain, isDev, listSettings, prisma, version } from './lib/common';
|
||||||
@@ -31,6 +32,7 @@ prisma.setting.findFirst().then(async (settings) => {
|
|||||||
logger: settings?.isAPIDebuggingEnabled || false,
|
logger: settings?.isAPIDebuggingEnabled || false,
|
||||||
trustProxy: true
|
trustProxy: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
required: ['COOLIFY_SECRET_KEY', 'COOLIFY_DATABASE_URL', 'COOLIFY_IS_ON'],
|
required: ['COOLIFY_SECRET_KEY', 'COOLIFY_DATABASE_URL', 'COOLIFY_IS_ON'],
|
||||||
@@ -88,13 +90,13 @@ prisma.setting.findFirst().then(async (settings) => {
|
|||||||
return reply.status(200).sendFile('index.html');
|
return reply.status(200).sendFile('index.html');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
fastify.register(multipart, { limits: { fileSize: 100000 } });
|
||||||
fastify.register(autoLoad, {
|
fastify.register(autoLoad, {
|
||||||
dir: join(__dirname, 'plugins')
|
dir: join(__dirname, 'plugins')
|
||||||
});
|
});
|
||||||
fastify.register(autoLoad, {
|
fastify.register(autoLoad, {
|
||||||
dir: join(__dirname, 'routes')
|
dir: join(__dirname, 'routes')
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.register(cookie)
|
fastify.register(cookie)
|
||||||
fastify.register(cors);
|
fastify.register(cors);
|
||||||
fastify.addHook('onRequest', async (request, reply) => {
|
fastify.addHook('onRequest', async (request, reply) => {
|
||||||
@@ -145,11 +147,16 @@ prisma.setting.findFirst().then(async (settings) => {
|
|||||||
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupStorage")
|
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupStorage")
|
||||||
}, isDev ? 6000 : 60000 * 10)
|
}, isDev ? 6000 : 60000 * 10)
|
||||||
|
|
||||||
// checkProxies
|
// checkProxies and checkFluentBit
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:checkProxies")
|
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:checkProxies")
|
||||||
|
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:checkFluentBit")
|
||||||
}, 10000)
|
}, 10000)
|
||||||
|
|
||||||
|
setInterval(async () => {
|
||||||
|
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:copySSLCertificates")
|
||||||
|
}, 2000)
|
||||||
|
|
||||||
// cleanupPrismaEngines
|
// cleanupPrismaEngines
|
||||||
// setInterval(async () => {
|
// setInterval(async () => {
|
||||||
// scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupPrismaEngines")
|
// scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupPrismaEngines")
|
||||||
|
|||||||
@@ -38,8 +38,16 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
for (const queueBuild of queuedBuilds) {
|
for (const queueBuild of queuedBuilds) {
|
||||||
actions.push(async () => {
|
actions.push(async () => {
|
||||||
let application = await prisma.application.findUnique({ where: { id: queueBuild.applicationId }, include: { destinationDocker: true, gitSource: { include: { githubApp: true, gitlabApp: true } }, persistentStorage: true, secrets: true, settings: true, teams: true } })
|
let application = await prisma.application.findUnique({ where: { id: queueBuild.applicationId }, include: { destinationDocker: true, gitSource: { include: { githubApp: true, gitlabApp: true } }, persistentStorage: true, secrets: true, settings: true, teams: true } })
|
||||||
let { id: buildId, type, sourceBranch = null, pullmergeRequestId = null, forceRebuild } = queueBuild
|
let { id: buildId, type, sourceBranch = null, pullmergeRequestId = null, previewApplicationId = null, forceRebuild } = queueBuild
|
||||||
application = decryptApplication(application)
|
application = decryptApplication(application)
|
||||||
|
const originalApplicationId = application.id
|
||||||
|
if (pullmergeRequestId) {
|
||||||
|
const previewApplications = await prisma.previewApplication.findMany({ where: { applicationId: originalApplicationId, pullmergeRequestId } })
|
||||||
|
if (previewApplications.length > 0) {
|
||||||
|
previewApplicationId = previewApplications[0].id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const usableApplicationId = previewApplicationId || originalApplicationId
|
||||||
try {
|
try {
|
||||||
if (queueBuild.status === 'running') {
|
if (queueBuild.status === 'running') {
|
||||||
await saveBuildLog({ line: 'Building halted, restarting...', buildId, applicationId: application.id });
|
await saveBuildLog({ line: 'Building halted, restarting...', buildId, applicationId: application.id });
|
||||||
@@ -104,17 +112,17 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
)
|
)
|
||||||
.digest('hex');
|
.digest('hex');
|
||||||
const { debug } = settings;
|
const { debug } = settings;
|
||||||
if (concurrency === 1) {
|
// if (concurrency === 1) {
|
||||||
await prisma.build.updateMany({
|
// await prisma.build.updateMany({
|
||||||
where: {
|
// where: {
|
||||||
status: { in: ['queued', 'running'] },
|
// status: { in: ['queued', 'running'] },
|
||||||
id: { not: buildId },
|
// id: { not: buildId },
|
||||||
applicationId,
|
// applicationId,
|
||||||
createdAt: { lt: new Date(new Date().getTime() - 10 * 1000) }
|
// createdAt: { lt: new Date(new Date().getTime() - 10 * 1000) }
|
||||||
},
|
// },
|
||||||
data: { status: 'failed' }
|
// data: { status: 'failed' }
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
let imageId = applicationId;
|
let imageId = applicationId;
|
||||||
let domain = getDomain(fqdn);
|
let domain = getDomain(fqdn);
|
||||||
const volumes =
|
const volumes =
|
||||||
@@ -146,7 +154,7 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
startCommand = configuration.startCommand;
|
startCommand = configuration.startCommand;
|
||||||
buildCommand = configuration.buildCommand;
|
buildCommand = configuration.buildCommand;
|
||||||
publishDirectory = configuration.publishDirectory;
|
publishDirectory = configuration.publishDirectory;
|
||||||
baseDirectory = configuration.baseDirectory;
|
baseDirectory = configuration.baseDirectory || '';
|
||||||
dockerFileLocation = configuration.dockerFileLocation;
|
dockerFileLocation = configuration.dockerFileLocation;
|
||||||
denoMainFile = configuration.denoMainFile;
|
denoMainFile = configuration.denoMainFile;
|
||||||
const commit = await importers[gitSource.type]({
|
const commit = await importers[gitSource.type]({
|
||||||
@@ -261,7 +269,10 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
if (secrets.length > 0) {
|
if (secrets.length > 0) {
|
||||||
secrets.forEach((secret) => {
|
secrets.forEach((secret) => {
|
||||||
if (pullmergeRequestId) {
|
if (pullmergeRequestId) {
|
||||||
if (secret.isPRMRSecret) {
|
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
|
||||||
|
if (isSecretFound.length > 0) {
|
||||||
|
envs.push(`${secret.name}=${isSecretFound[0].value}`);
|
||||||
|
} else {
|
||||||
envs.push(`${secret.name}=${secret.value}`);
|
envs.push(`${secret.name}=${secret.value}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -335,10 +346,15 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
|
await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await saveBuildLog({ line: error, buildId, applicationId });
|
await saveBuildLog({ line: error, buildId, applicationId });
|
||||||
await prisma.build.updateMany({
|
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } })
|
||||||
where: { id: buildId, status: { in: ['queued', 'running'] } },
|
if (foundBuild) {
|
||||||
data: { status: 'failed' }
|
await prisma.build.update({
|
||||||
});
|
where: { id: buildId },
|
||||||
|
data: {
|
||||||
|
status: 'failed'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
}
|
}
|
||||||
await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId });
|
await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId });
|
||||||
@@ -350,11 +366,18 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
await prisma.build.updateMany({
|
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } })
|
||||||
where: { id: buildId, status: { in: ['queued', 'running'] } },
|
if (foundBuild) {
|
||||||
data: { status: 'failed' }
|
await prisma.build.update({
|
||||||
});
|
where: { id: buildId },
|
||||||
await saveBuildLog({ line: error, buildId, applicationId: application.id });
|
data: {
|
||||||
|
status: 'failed'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (error !== 1) {
|
||||||
|
await saveBuildLog({ line: error, buildId, applicationId: application.id });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { parentPort } from 'node:worker_threads';
|
import { parentPort } from 'node:worker_threads';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { compareVersions } from 'compare-versions';
|
import { compareVersions } from 'compare-versions';
|
||||||
import { asyncExecShell, cleanupDockerStorage, executeDockerCmd, isDev, prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, listSettings, version, createRemoteEngineConfiguration } from '../lib/common';
|
import { asyncExecShell, cleanupDockerStorage, executeDockerCmd, isDev, prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, listSettings, version, createRemoteEngineConfiguration, decrypt, executeSSHCmd } from '../lib/common';
|
||||||
|
import { checkContainer } from '../lib/docker';
|
||||||
|
import fs from 'fs/promises'
|
||||||
async function autoUpdater() {
|
async function autoUpdater() {
|
||||||
try {
|
try {
|
||||||
const currentVersion = version;
|
const currentVersion = version;
|
||||||
@@ -21,14 +22,17 @@ async function autoUpdater() {
|
|||||||
const activeCount = 0
|
const activeCount = 0
|
||||||
if (activeCount === 0) {
|
if (activeCount === 0) {
|
||||||
if (!isDev) {
|
if (!isDev) {
|
||||||
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
|
const { isAutoUpdateEnabled } = await prisma.setting.findFirst();
|
||||||
await asyncExecShell(`env | grep COOLIFY > .env`);
|
if (isAutoUpdateEnabled) {
|
||||||
await asyncExecShell(
|
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
|
||||||
`sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=true' .env`
|
await asyncExecShell(`env | grep COOLIFY > .env`);
|
||||||
);
|
await asyncExecShell(
|
||||||
await asyncExecShell(
|
`sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env`
|
||||||
`docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify && docker rm coolify && docker compose up -d --force-recreate"`
|
);
|
||||||
);
|
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-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"`
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('Updating (not really in dev mode).');
|
console.log('Updating (not really in dev mode).');
|
||||||
}
|
}
|
||||||
@@ -36,6 +40,68 @@ async function autoUpdater() {
|
|||||||
}
|
}
|
||||||
} catch (error) { }
|
} catch (error) { }
|
||||||
}
|
}
|
||||||
|
async function checkFluentBit() {
|
||||||
|
if (!isDev) {
|
||||||
|
const engine = '/var/run/docker.sock';
|
||||||
|
const { id } = await prisma.destinationDocker.findFirst({
|
||||||
|
where: { engine, network: 'coolify' }
|
||||||
|
});
|
||||||
|
const { found } = await checkContainer({ dockerId: id, container: 'coolify-fluentbit' });
|
||||||
|
if (!found) {
|
||||||
|
await asyncExecShell(`env | grep COOLIFY > .env`);
|
||||||
|
await asyncExecShell(`docker compose up -d fluent-bit`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function copyRemoteCertificates(id: string, dockerId: string, remoteIpAddress: string) {
|
||||||
|
try {
|
||||||
|
await asyncExecShell(`scp /tmp/${id}-cert.pem /tmp/${id}-key.pem ${remoteIpAddress}:/tmp/`)
|
||||||
|
await executeSSHCmd({ dockerId, command: `docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'` })
|
||||||
|
await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/` })
|
||||||
|
await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/` })
|
||||||
|
} catch (error) {
|
||||||
|
console.log({ error })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function copyLocalCertificates(id: string) {
|
||||||
|
try {
|
||||||
|
await asyncExecShell(`docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'`)
|
||||||
|
await asyncExecShell(`docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/`)
|
||||||
|
await asyncExecShell(`docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/`)
|
||||||
|
} catch (error) {
|
||||||
|
console.log({ error })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function copySSLCertificates() {
|
||||||
|
try {
|
||||||
|
const pAll = await import('p-all');
|
||||||
|
const actions = []
|
||||||
|
const certificates = await prisma.certificate.findMany({ include: { team: true } })
|
||||||
|
const teamIds = certificates.map(c => c.teamId)
|
||||||
|
const destinations = await prisma.destinationDocker.findMany({ where: { isCoolifyProxyUsed: true, teams: { some: { id: { in: [...teamIds] } } } } })
|
||||||
|
for (const certificate of certificates) {
|
||||||
|
const { id, key, cert } = certificate
|
||||||
|
const decryptedKey = decrypt(key)
|
||||||
|
await fs.writeFile(`/tmp/${id}-key.pem`, decryptedKey)
|
||||||
|
await fs.writeFile(`/tmp/${id}-cert.pem`, cert)
|
||||||
|
for (const destination of destinations) {
|
||||||
|
if (destination.remoteEngine) {
|
||||||
|
if (destination.remoteVerified) {
|
||||||
|
const { id: dockerId, remoteIpAddress } = destination
|
||||||
|
actions.push(async () => copyRemoteCertificates(id, dockerId, remoteIpAddress))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
actions.push(async () => copyLocalCertificates(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await pAll.default(actions, { concurrency: 1 })
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
} finally {
|
||||||
|
await asyncExecShell(`find /tmp/ -maxdepth 1 -type f -name '*-*.pem' -delete`)
|
||||||
|
}
|
||||||
|
}
|
||||||
async function checkProxies() {
|
async function checkProxies() {
|
||||||
try {
|
try {
|
||||||
const { default: isReachable } = await import('is-port-reachable');
|
const { default: isReachable } = await import('is-port-reachable');
|
||||||
@@ -186,7 +252,8 @@ async function cleanupStorage() {
|
|||||||
(async () => {
|
(async () => {
|
||||||
let status = {
|
let status = {
|
||||||
cleanupStorage: false,
|
cleanupStorage: false,
|
||||||
autoUpdater: false
|
autoUpdater: false,
|
||||||
|
copySSLCertificates: false,
|
||||||
}
|
}
|
||||||
if (parentPort) {
|
if (parentPort) {
|
||||||
parentPort.on('message', async (message) => {
|
parentPort.on('message', async (message) => {
|
||||||
@@ -212,6 +279,18 @@ async function cleanupStorage() {
|
|||||||
await checkProxies();
|
await checkProxies();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (message === 'action:checkFluentBit') {
|
||||||
|
await checkFluentBit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (message === 'action:copySSLCertificates') {
|
||||||
|
if (!status.copySSLCertificates) {
|
||||||
|
status.copySSLCertificates = true
|
||||||
|
await copySSLCertificates();
|
||||||
|
status.copySSLCertificates = false
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (message === 'action:autoUpdater') {
|
if (message === 'action:autoUpdater') {
|
||||||
if (!status.cleanupStorage) {
|
if (!status.cleanupStorage) {
|
||||||
status.autoUpdater = true
|
status.autoUpdater = true
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { base64Encode, executeDockerCmd, generateTimestamp, getDomain, isDev, prisma, version } from "../common";
|
import { base64Encode, encrypt, executeDockerCmd, generateTimestamp, getDomain, isDev, prisma, version } from "../common";
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import { day } from "../dayjs";
|
import { day } from "../dayjs";
|
||||||
|
|
||||||
@@ -342,13 +342,13 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st
|
|||||||
}
|
}
|
||||||
if (buildPack === 'laravel') {
|
if (buildPack === 'laravel') {
|
||||||
payload.baseImage = 'webdevops/php-apache:8.2-alpine';
|
payload.baseImage = 'webdevops/php-apache:8.2-alpine';
|
||||||
|
payload.baseImages = phpVersions;
|
||||||
payload.baseBuildImage = 'node:18';
|
payload.baseBuildImage = 'node:18';
|
||||||
payload.baseBuildImages = nodeVersions;
|
payload.baseBuildImages = nodeVersions;
|
||||||
}
|
}
|
||||||
if (buildPack === 'heroku') {
|
if (buildPack === 'heroku') {
|
||||||
payload.baseImage = 'heroku/buildpacks:20';
|
payload.baseImage = 'heroku/buildpacks:20';
|
||||||
payload.baseImages = herokuVersions;
|
payload.baseImages = herokuVersions;
|
||||||
|
|
||||||
}
|
}
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
@@ -384,7 +384,7 @@ export const setDefaultConfiguration = async (data: any) => {
|
|||||||
if (!publishDirectory) publishDirectory = template?.publishDirectory || null;
|
if (!publishDirectory) publishDirectory = template?.publishDirectory || null;
|
||||||
if (baseDirectory) {
|
if (baseDirectory) {
|
||||||
if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`;
|
if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`;
|
||||||
if (!baseDirectory.endsWith('/')) baseDirectory = `${baseDirectory}/`;
|
if (baseDirectory.endsWith('/') && baseDirectory !== '/') baseDirectory = baseDirectory.slice(0, -1);
|
||||||
}
|
}
|
||||||
if (dockerFileLocation) {
|
if (dockerFileLocation) {
|
||||||
if (!dockerFileLocation.startsWith('/')) dockerFileLocation = `/${dockerFileLocation}`;
|
if (!dockerFileLocation.startsWith('/')) dockerFileLocation = `/${dockerFileLocation}`;
|
||||||
@@ -461,17 +461,32 @@ export const saveBuildLog = async ({
|
|||||||
buildId: string;
|
buildId: string;
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}): Promise<any> => {
|
}): Promise<any> => {
|
||||||
|
const { default: got } = await import('got')
|
||||||
|
|
||||||
if (line && typeof line === 'string' && line.includes('ghs_')) {
|
if (line && typeof line === 'string' && line.includes('ghs_')) {
|
||||||
const regex = /ghs_.*@/g;
|
const regex = /ghs_.*@/g;
|
||||||
line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
|
line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
|
||||||
}
|
}
|
||||||
const addTimestamp = `[${generateTimestamp()}] ${line}`;
|
const addTimestamp = `[${generateTimestamp()}] ${line}`;
|
||||||
if (isDev) console.debug(`[${applicationId}] ${addTimestamp}`);
|
const fluentBitUrl = isDev ? 'http://localhost:24224' : 'http://coolify-fluentbit:24224';
|
||||||
return await prisma.buildLog.create({
|
|
||||||
data: {
|
if (isDev) {
|
||||||
line: addTimestamp, buildId, time: Number(day().valueOf()), applicationId
|
console.debug(`[${applicationId}] ${addTimestamp}`);
|
||||||
}
|
}
|
||||||
});
|
try {
|
||||||
|
return await got.post(`${fluentBitUrl}/${applicationId}_buildlog_${buildId}.csv`, {
|
||||||
|
json: {
|
||||||
|
line: encrypt(line)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch(error) {
|
||||||
|
return await prisma.buildLog.create({
|
||||||
|
data: {
|
||||||
|
line: addTimestamp, buildId, time: Number(day().valueOf()), applicationId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function copyBaseConfigurationFiles(
|
export async function copyBaseConfigurationFiles(
|
||||||
@@ -556,7 +571,6 @@ export function checkPnpm(installCommand = null, buildCommand = null, startComma
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function buildImage({
|
export async function buildImage({
|
||||||
applicationId,
|
applicationId,
|
||||||
tag,
|
tag,
|
||||||
@@ -677,8 +691,6 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
|
|||||||
secrets,
|
secrets,
|
||||||
pullmergeRequestId
|
pullmergeRequestId
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
|
|
||||||
const isPnpm = checkPnpm(installCommand, buildCommand);
|
const isPnpm = checkPnpm(installCommand, buildCommand);
|
||||||
const Dockerfile: Array<string> = [];
|
const Dockerfile: Array<string> = [];
|
||||||
Dockerfile.push(`FROM ${imageForBuild}`);
|
Dockerfile.push(`FROM ${imageForBuild}`);
|
||||||
@@ -688,7 +700,10 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
|
|||||||
secrets.forEach((secret) => {
|
secrets.forEach((secret) => {
|
||||||
if (secret.isBuildSecret) {
|
if (secret.isBuildSecret) {
|
||||||
if (pullmergeRequestId) {
|
if (pullmergeRequestId) {
|
||||||
if (secret.isPRMRSecret) {
|
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
|
||||||
|
if (isSecretFound.length > 0) {
|
||||||
|
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
|
||||||
|
} else {
|
||||||
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -722,7 +737,10 @@ export async function buildCacheImageForLaravel(data, imageForBuild) {
|
|||||||
secrets.forEach((secret) => {
|
secrets.forEach((secret) => {
|
||||||
if (secret.isBuildSecret) {
|
if (secret.isBuildSecret) {
|
||||||
if (pullmergeRequestId) {
|
if (pullmergeRequestId) {
|
||||||
if (secret.isPRMRSecret) {
|
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
|
||||||
|
if (isSecretFound.length > 0) {
|
||||||
|
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
|
||||||
|
} else {
|
||||||
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ const createDockerfile = async (data, image): Promise<void> => {
|
|||||||
secrets.forEach((secret) => {
|
secrets.forEach((secret) => {
|
||||||
if (secret.isBuildSecret) {
|
if (secret.isBuildSecret) {
|
||||||
if (pullmergeRequestId) {
|
if (pullmergeRequestId) {
|
||||||
if (secret.isPRMRSecret) {
|
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
|
||||||
|
if (isSecretFound.length > 0) {
|
||||||
|
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
|
||||||
|
} else {
|
||||||
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -14,12 +14,8 @@ export default async function (data) {
|
|||||||
dockerFileLocation
|
dockerFileLocation
|
||||||
} = data
|
} = data
|
||||||
try {
|
try {
|
||||||
const file = `${workdir}${dockerFileLocation}`;
|
const file = `${workdir}${baseDirectory}${dockerFileLocation}`;
|
||||||
let dockerFileOut = `${workdir}`;
|
data.workdir = `${workdir}${baseDirectory}`;
|
||||||
if (baseDirectory) {
|
|
||||||
dockerFileOut = `${workdir}${baseDirectory}`;
|
|
||||||
workdir = `${workdir}${baseDirectory}`;
|
|
||||||
}
|
|
||||||
const Dockerfile: Array<string> = (await fs.readFile(`${file}`, 'utf8'))
|
const Dockerfile: Array<string> = (await fs.readFile(`${file}`, 'utf8'))
|
||||||
.toString()
|
.toString()
|
||||||
.trim()
|
.trim()
|
||||||
@@ -44,7 +40,7 @@ export default async function (data) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.writeFile(`${dockerFileOut}${dockerFileLocation}`, Dockerfile.join('\n'));
|
await fs.writeFile(`${workdir}${dockerFileLocation}`, Dockerfile.join('\n'));
|
||||||
await buildImage(data);
|
await buildImage(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -2,38 +2,16 @@ import { executeDockerCmd, prisma } from "../common"
|
|||||||
import { saveBuildLog } from "./common";
|
import { saveBuildLog } from "./common";
|
||||||
|
|
||||||
export default async function (data: any): Promise<void> {
|
export default async function (data: any): Promise<void> {
|
||||||
const { buildId, applicationId, tag, dockerId, debug, workdir } = data
|
const { buildId, applicationId, tag, dockerId, debug, workdir, baseDirectory } = data
|
||||||
try {
|
try {
|
||||||
|
|
||||||
await saveBuildLog({ line: `Building image started.`, buildId, applicationId });
|
await saveBuildLog({ line: `Building image started.`, buildId, applicationId });
|
||||||
const { stdout } = await executeDockerCmd({
|
await executeDockerCmd({
|
||||||
|
debug,
|
||||||
dockerId,
|
dockerId,
|
||||||
command: `pack build -p ${workdir} ${applicationId}:${tag} --builder heroku/buildpacks:20`
|
command: `pack build -p ${workdir}${baseDirectory} ${applicationId}:${tag} --builder heroku/buildpacks:20`
|
||||||
})
|
})
|
||||||
if (debug) {
|
|
||||||
const array = stdout.split('\n')
|
|
||||||
for (const line of array) {
|
|
||||||
if (line !== '\n') {
|
|
||||||
await saveBuildLog({
|
|
||||||
line: `${line.replace('\n', '')}`,
|
|
||||||
buildId,
|
|
||||||
applicationId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await saveBuildLog({ line: `Building image successful.`, buildId, applicationId });
|
await saveBuildLog({ line: `Building image successful.`, buildId, applicationId });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const array = error.stdout.split('\n')
|
|
||||||
for (const line of array) {
|
|
||||||
if (line !== '\n') {
|
|
||||||
await saveBuildLog({
|
|
||||||
line: `${line.replace('\n', '')}`,
|
|
||||||
buildId,
|
|
||||||
applicationId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ const createDockerfile = async (data, image): Promise<void> => {
|
|||||||
secrets.forEach((secret) => {
|
secrets.forEach((secret) => {
|
||||||
if (secret.isBuildSecret) {
|
if (secret.isBuildSecret) {
|
||||||
if (pullmergeRequestId) {
|
if (pullmergeRequestId) {
|
||||||
if (secret.isPRMRSecret) {
|
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
|
||||||
|
if (isSecretFound.length > 0) {
|
||||||
|
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
|
||||||
|
} else {
|
||||||
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ const createDockerfile = async (data, image): Promise<void> => {
|
|||||||
secrets.forEach((secret) => {
|
secrets.forEach((secret) => {
|
||||||
if (secret.isBuildSecret) {
|
if (secret.isBuildSecret) {
|
||||||
if (pullmergeRequestId) {
|
if (pullmergeRequestId) {
|
||||||
if (secret.isPRMRSecret) {
|
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
|
||||||
|
if (isSecretFound.length > 0) {
|
||||||
|
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
|
||||||
|
} else {
|
||||||
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ const createDockerfile = async (data, image): Promise<void> => {
|
|||||||
secrets.forEach((secret) => {
|
secrets.forEach((secret) => {
|
||||||
if (secret.isBuildSecret) {
|
if (secret.isBuildSecret) {
|
||||||
if (pullmergeRequestId) {
|
if (pullmergeRequestId) {
|
||||||
if (secret.isPRMRSecret) {
|
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
|
||||||
|
if (isSecretFound.length > 0) {
|
||||||
|
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
|
||||||
|
} else {
|
||||||
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
|
|||||||
secrets.forEach((secret) => {
|
secrets.forEach((secret) => {
|
||||||
if (secret.isBuildSecret) {
|
if (secret.isBuildSecret) {
|
||||||
if (pullmergeRequestId) {
|
if (pullmergeRequestId) {
|
||||||
if (secret.isPRMRSecret) {
|
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
|
||||||
|
if (isSecretFound.length > 0) {
|
||||||
|
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
|
||||||
|
} else {
|
||||||
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ const createDockerfile = async (data, image): Promise<void> => {
|
|||||||
secrets.forEach((secret) => {
|
secrets.forEach((secret) => {
|
||||||
if (secret.isBuildSecret) {
|
if (secret.isBuildSecret) {
|
||||||
if (pullmergeRequestId) {
|
if (pullmergeRequestId) {
|
||||||
if (secret.isPRMRSecret) {
|
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
|
||||||
|
if (isSecretFound.length > 0) {
|
||||||
|
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
|
||||||
|
} else {
|
||||||
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ const createDockerfile = async (data, image): Promise<void> => {
|
|||||||
secrets.forEach((secret) => {
|
secrets.forEach((secret) => {
|
||||||
if (secret.isBuildSecret) {
|
if (secret.isBuildSecret) {
|
||||||
if (pullmergeRequestId) {
|
if (pullmergeRequestId) {
|
||||||
if (secret.isPRMRSecret) {
|
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
|
||||||
|
if (isSecretFound.length > 0) {
|
||||||
|
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
|
||||||
|
} else {
|
||||||
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@ export function formatLabelsOnDocker(data) {
|
|||||||
return container
|
return container
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
export async function checkContainer({ dockerId, container, remove = false }: { dockerId: string, container: string, remove?: boolean }): Promise<boolean> {
|
export async function checkContainer({ dockerId, container, remove = false }: { dockerId: string, container: string, remove?: boolean }): Promise<{ found: boolean, status?: { isExited: boolean, isRunning: boolean, isRestarting: boolean } }> {
|
||||||
let containerFound = false;
|
let containerFound = false;
|
||||||
try {
|
try {
|
||||||
const { stdout } = await executeDockerCmd({
|
const { stdout } = await executeDockerCmd({
|
||||||
@@ -21,10 +21,12 @@ export async function checkContainer({ dockerId, container, remove = false }: {
|
|||||||
command:
|
command:
|
||||||
`docker inspect --format '{{json .State}}' ${container}`
|
`docker inspect --format '{{json .State}}' ${container}`
|
||||||
});
|
});
|
||||||
|
containerFound = true
|
||||||
const parsedStdout = JSON.parse(stdout);
|
const parsedStdout = JSON.parse(stdout);
|
||||||
const status = parsedStdout.Status;
|
const status = parsedStdout.Status;
|
||||||
const isRunning = status === 'running';
|
const isRunning = status === 'running';
|
||||||
|
const isRestarting = status === 'restarting'
|
||||||
|
const isExited = status === 'exited'
|
||||||
if (status === 'created') {
|
if (status === 'created') {
|
||||||
await executeDockerCmd({
|
await executeDockerCmd({
|
||||||
dockerId,
|
dockerId,
|
||||||
@@ -39,13 +41,23 @@ export async function checkContainer({ dockerId, container, remove = false }: {
|
|||||||
`docker rm ${container}`
|
`docker rm ${container}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (isRunning) {
|
|
||||||
containerFound = true;
|
return {
|
||||||
}
|
found: containerFound,
|
||||||
|
status: {
|
||||||
|
isRunning,
|
||||||
|
isRestarting,
|
||||||
|
isExited
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Container not found
|
// Container not found
|
||||||
}
|
}
|
||||||
return containerFound;
|
return {
|
||||||
|
found: false
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function isContainerExited(dockerId: string, containerName: string): Promise<boolean> {
|
export async function isContainerExited(dockerId: string, containerName: string): Promise<boolean> {
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ export default async function ({
|
|||||||
branch,
|
branch,
|
||||||
buildId,
|
buildId,
|
||||||
privateSshKey,
|
privateSshKey,
|
||||||
customPort
|
customPort,
|
||||||
|
forPublic
|
||||||
}: {
|
}: {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
workdir: string;
|
workdir: string;
|
||||||
@@ -21,11 +22,15 @@ export default async function ({
|
|||||||
repodir: string;
|
repodir: string;
|
||||||
privateSshKey: string;
|
privateSshKey: string;
|
||||||
customPort: number;
|
customPort: number;
|
||||||
|
forPublic: boolean;
|
||||||
}): Promise<string> {
|
}): Promise<string> {
|
||||||
const url = htmlUrl.replace('https://', '').replace('http://', '').replace(/\/$/, '');
|
const url = htmlUrl.replace('https://', '').replace('http://', '').replace(/\/$/, '');
|
||||||
await saveBuildLog({ line: 'GitLab importer started.', buildId, applicationId });
|
await saveBuildLog({ line: 'GitLab importer started.', buildId, applicationId });
|
||||||
await asyncExecShell(`echo '${privateSshKey}' > ${repodir}/id.rsa`);
|
|
||||||
await asyncExecShell(`chmod 600 ${repodir}/id.rsa`);
|
if (!forPublic) {
|
||||||
|
await asyncExecShell(`echo '${privateSshKey}' > ${repodir}/id.rsa`);
|
||||||
|
await asyncExecShell(`chmod 600 ${repodir}/id.rsa`);
|
||||||
|
}
|
||||||
|
|
||||||
await saveBuildLog({
|
await saveBuildLog({
|
||||||
line: `Cloning ${repository}:${branch} branch.`,
|
line: `Cloning ${repository}:${branch} branch.`,
|
||||||
@@ -33,9 +38,16 @@ export default async function ({
|
|||||||
applicationId
|
applicationId
|
||||||
});
|
});
|
||||||
|
|
||||||
await asyncExecShell(
|
if (forPublic) {
|
||||||
`git clone -q -b ${branch} git@${url}:${repository}.git --config core.sshCommand="ssh -p ${customPort} -q -i ${repodir}id.rsa -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git submodule update --init --recursive && git lfs pull && cd .. `
|
await asyncExecShell(
|
||||||
);
|
`git clone -q -b ${branch} https://${url}/${repository}.git ${workdir}/ && cd ${workdir}/ && git submodule update --init --recursive && git lfs pull && cd .. `
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await asyncExecShell(
|
||||||
|
`git clone -q -b ${branch} git@${url}:${repository}.git --config core.sshCommand="ssh -p ${customPort} -q -i ${repodir}id.rsa -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git submodule update --init --recursive && git lfs pull && cd .. `
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`);
|
const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`);
|
||||||
return commit.replace('\n', '');
|
return commit.replace('\n', '');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ Bree.extend(TSBree);
|
|||||||
|
|
||||||
const options: any = {
|
const options: any = {
|
||||||
defaultExtension: 'js',
|
defaultExtension: 'js',
|
||||||
// logger: new Cabin(),
|
logger: new Cabin(),
|
||||||
logger: false,
|
// logger: false,
|
||||||
workerMessageHandler: async ({ name, message }) => {
|
workerMessageHandler: async ({ name, message }) => {
|
||||||
if (name === 'deployApplication' && message?.deploying) {
|
if (name === 'deployApplication' && message?.deploying) {
|
||||||
if (scheduler.workers.has('autoUpdater') || scheduler.workers.has('cleanupStorage')) {
|
if (scheduler.workers.has('autoUpdater') || scheduler.workers.has('cleanupStorage')) {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const includeServices: any = {
|
|||||||
glitchTip: true,
|
glitchTip: true,
|
||||||
searxng: true,
|
searxng: true,
|
||||||
weblate: true,
|
weblate: true,
|
||||||
taiga: true
|
taiga: true,
|
||||||
};
|
};
|
||||||
export async function configureServiceType({
|
export async function configureServiceType({
|
||||||
id,
|
id,
|
||||||
@@ -378,6 +378,6 @@ export async function removeService({ id }: { id: string }): Promise<void> {
|
|||||||
await prisma.searxng.deleteMany({ where: { serviceId: id } });
|
await prisma.searxng.deleteMany({ where: { serviceId: id } });
|
||||||
await prisma.weblate.deleteMany({ where: { serviceId: id } });
|
await prisma.weblate.deleteMany({ where: { serviceId: id } });
|
||||||
await prisma.taiga.deleteMany({ where: { serviceId: id } });
|
await prisma.taiga.deleteMany({ where: { serviceId: id } });
|
||||||
|
|
||||||
await prisma.service.delete({ where: { id } });
|
await prisma.service.delete({ where: { id } });
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import bcrypt from 'bcryptjs';
|
|||||||
import { ServiceStartStop } from '../../routes/api/v1/services/types';
|
import { ServiceStartStop } from '../../routes/api/v1/services/types';
|
||||||
import { asyncSleep, ComposeFile, createDirectories, defaultComposeConfiguration, errorHandler, executeDockerCmd, getDomain, getFreePublicPort, getServiceFromDB, getServiceImage, getServiceMainPort, isARM, isDev, makeLabelForServices, persistentVolumes, prisma } from '../common';
|
import { asyncSleep, ComposeFile, createDirectories, defaultComposeConfiguration, errorHandler, executeDockerCmd, getDomain, getFreePublicPort, getServiceFromDB, getServiceImage, getServiceMainPort, isARM, isDev, makeLabelForServices, persistentVolumes, prisma } from '../common';
|
||||||
import { defaultServiceConfigurations } from '../services';
|
import { defaultServiceConfigurations } from '../services';
|
||||||
|
import { OnlyId } from '../../types';
|
||||||
|
|
||||||
export async function startService(request: FastifyRequest<ServiceStartStop>) {
|
export async function startService(request: FastifyRequest<ServiceStartStop>) {
|
||||||
try {
|
try {
|
||||||
@@ -69,6 +70,13 @@ export async function startService(request: FastifyRequest<ServiceStartStop>) {
|
|||||||
if (type === 'taiga') {
|
if (type === 'taiga') {
|
||||||
return await startTaigaService(request)
|
return await startTaigaService(request)
|
||||||
}
|
}
|
||||||
|
if (type === 'grafana') {
|
||||||
|
return await startGrafanaService(request)
|
||||||
|
}
|
||||||
|
if (type === 'trilium') {
|
||||||
|
return await startTriliumService(request)
|
||||||
|
}
|
||||||
|
|
||||||
throw `Service type ${type} not supported.`
|
throw `Service type ${type} not supported.`
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw { status: 500, message: error?.message || error }
|
throw { status: 500, message: error?.message || error }
|
||||||
@@ -314,15 +322,15 @@ async function startMinioService(request: FastifyRequest<ServiceStartStop>) {
|
|||||||
destinationDocker,
|
destinationDocker,
|
||||||
persistentStorage,
|
persistentStorage,
|
||||||
exposePort,
|
exposePort,
|
||||||
minio: { rootUser, rootUserPassword },
|
minio: { rootUser, rootUserPassword, apiFqdn },
|
||||||
serviceSecret
|
serviceSecret
|
||||||
} = service;
|
} = service;
|
||||||
|
|
||||||
const network = destinationDockerId && destinationDocker.network;
|
const network = destinationDockerId && destinationDocker.network;
|
||||||
const port = getServiceMainPort('minio');
|
const port = getServiceMainPort('minio');
|
||||||
|
|
||||||
const { service: { destinationDocker: { id: dockerId } } } = await prisma.minio.findUnique({ where: { serviceId: id }, include: { service: { include: { destinationDocker: true } } } })
|
const { service: { destinationDocker: { remoteEngine, engine, remoteIpAddress } } } = await prisma.minio.findUnique({ where: { serviceId: id }, include: { service: { include: { destinationDocker: true } } } })
|
||||||
const publicPort = await getFreePublicPort(id, dockerId);
|
const publicPort = await getFreePublicPort({ id, remoteEngine, engine, remoteIpAddress });
|
||||||
|
|
||||||
const consolePort = 9001;
|
const consolePort = 9001;
|
||||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||||
@@ -333,7 +341,7 @@ async function startMinioService(request: FastifyRequest<ServiceStartStop>) {
|
|||||||
image: `${image}:${version}`,
|
image: `${image}:${version}`,
|
||||||
volumes: [`${id}-minio-data:/data`],
|
volumes: [`${id}-minio-data:/data`],
|
||||||
environmentVariables: {
|
environmentVariables: {
|
||||||
MINIO_SERVER_URL: fqdn,
|
MINIO_SERVER_URL: apiFqdn,
|
||||||
MINIO_DOMAIN: getDomain(fqdn),
|
MINIO_DOMAIN: getDomain(fqdn),
|
||||||
MINIO_ROOT_USER: rootUser,
|
MINIO_ROOT_USER: rootUser,
|
||||||
MINIO_ROOT_PASSWORD: rootUserPassword,
|
MINIO_ROOT_PASSWORD: rootUserPassword,
|
||||||
@@ -655,7 +663,7 @@ async function startLanguageToolService(request: FastifyRequest<ServiceStartStop
|
|||||||
image: config.languagetool.image,
|
image: config.languagetool.image,
|
||||||
environment: config.languagetool.environmentVariables,
|
environment: config.languagetool.environmentVariables,
|
||||||
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
|
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
|
||||||
volumes: config.languagetool,
|
volumes: config.languagetool.volumes,
|
||||||
labels: makeLabelForServices('languagetool'),
|
labels: makeLabelForServices('languagetool'),
|
||||||
...defaultComposeConfiguration(network),
|
...defaultComposeConfiguration(network),
|
||||||
}
|
}
|
||||||
@@ -710,7 +718,7 @@ async function startN8nService(request: FastifyRequest<ServiceStartStop>) {
|
|||||||
[id]: {
|
[id]: {
|
||||||
container_name: id,
|
container_name: id,
|
||||||
image: config.n8n.image,
|
image: config.n8n.image,
|
||||||
volumes: config.n8n,
|
volumes: config.n8n.volumes,
|
||||||
environment: config.n8n.environmentVariables,
|
environment: config.n8n.environmentVariables,
|
||||||
labels: makeLabelForServices('n8n'),
|
labels: makeLabelForServices('n8n'),
|
||||||
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
|
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
|
||||||
@@ -900,8 +908,8 @@ async function startMeilisearchService(request: FastifyRequest<ServiceStartStop>
|
|||||||
const {
|
const {
|
||||||
meiliSearch: { masterKey }
|
meiliSearch: { masterKey }
|
||||||
} = service;
|
} = service;
|
||||||
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } =
|
const { type, version, destinationDockerId, destinationDocker,
|
||||||
service;
|
serviceSecret, exposePort, persistentStorage } = service;
|
||||||
const network = destinationDockerId && destinationDocker.network;
|
const network = destinationDockerId && destinationDocker.network;
|
||||||
const port = getServiceMainPort('meilisearch');
|
const port = getServiceMainPort('meilisearch');
|
||||||
|
|
||||||
@@ -1006,79 +1014,136 @@ async function startUmamiService(request: FastifyRequest<ServiceStartStop>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const initDbSQL = `
|
const initDbSQL = `
|
||||||
drop table if exists event;
|
-- CreateTable
|
||||||
drop table if exists pageview;
|
CREATE TABLE "account" (
|
||||||
drop table if exists session;
|
"user_id" SERIAL NOT NULL,
|
||||||
drop table if exists website;
|
"username" VARCHAR(255) NOT NULL,
|
||||||
drop table if exists account;
|
"password" VARCHAR(60) NOT NULL,
|
||||||
|
"is_admin" BOOLEAN NOT NULL DEFAULT false,
|
||||||
create table account (
|
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||||
user_id serial primary key,
|
"updated_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||||
username varchar(255) unique not null,
|
|
||||||
password varchar(60) not null,
|
PRIMARY KEY ("user_id")
|
||||||
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
|
-- CreateTable
|
||||||
);
|
CREATE TABLE "event" (
|
||||||
|
"event_id" SERIAL NOT NULL,
|
||||||
create table website (
|
"website_id" INTEGER NOT NULL,
|
||||||
website_id serial primary key,
|
"session_id" INTEGER NOT NULL,
|
||||||
website_uuid uuid unique not null,
|
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||||
user_id int not null references account(user_id) on delete cascade,
|
"url" VARCHAR(500) NOT NULL,
|
||||||
name varchar(100) not null,
|
"event_type" VARCHAR(50) NOT NULL,
|
||||||
domain varchar(500),
|
"event_value" VARCHAR(50) NOT NULL,
|
||||||
share_id varchar(64) unique,
|
|
||||||
created_at timestamp with time zone default current_timestamp
|
PRIMARY KEY ("event_id")
|
||||||
);
|
);
|
||||||
|
|
||||||
create table session (
|
-- CreateTable
|
||||||
session_id serial primary key,
|
CREATE TABLE "pageview" (
|
||||||
session_uuid uuid unique not null,
|
"view_id" SERIAL NOT NULL,
|
||||||
website_id int not null references website(website_id) on delete cascade,
|
"website_id" INTEGER NOT NULL,
|
||||||
created_at timestamp with time zone default current_timestamp,
|
"session_id" INTEGER NOT NULL,
|
||||||
hostname varchar(100),
|
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||||
browser varchar(20),
|
"url" VARCHAR(500) NOT NULL,
|
||||||
os varchar(20),
|
"referrer" VARCHAR(500),
|
||||||
device varchar(20),
|
|
||||||
screen varchar(11),
|
PRIMARY KEY ("view_id")
|
||||||
language varchar(35),
|
);
|
||||||
country char(2)
|
|
||||||
);
|
-- CreateTable
|
||||||
|
CREATE TABLE "session" (
|
||||||
create table pageview (
|
"session_id" SERIAL NOT NULL,
|
||||||
view_id serial primary key,
|
"session_uuid" UUID NOT NULL,
|
||||||
website_id int not null references website(website_id) on delete cascade,
|
"website_id" INTEGER NOT NULL,
|
||||||
session_id int not null references session(session_id) on delete cascade,
|
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||||
created_at timestamp with time zone default current_timestamp,
|
"hostname" VARCHAR(100),
|
||||||
url varchar(500) not null,
|
"browser" VARCHAR(20),
|
||||||
referrer varchar(500)
|
"os" VARCHAR(20),
|
||||||
);
|
"device" VARCHAR(20),
|
||||||
|
"screen" VARCHAR(11),
|
||||||
create table event (
|
"language" VARCHAR(35),
|
||||||
event_id serial primary key,
|
"country" CHAR(2),
|
||||||
website_id int not null references website(website_id) on delete cascade,
|
|
||||||
session_id int not null references session(session_id) on delete cascade,
|
PRIMARY KEY ("session_id")
|
||||||
created_at timestamp with time zone default current_timestamp,
|
);
|
||||||
url varchar(500) not null,
|
|
||||||
event_type varchar(50) not null,
|
-- CreateTable
|
||||||
event_value varchar(50) not null
|
CREATE TABLE "website" (
|
||||||
);
|
"website_id" SERIAL NOT NULL,
|
||||||
|
"website_uuid" UUID NOT NULL,
|
||||||
create index website_user_id_idx on website(user_id);
|
"user_id" INTEGER NOT NULL,
|
||||||
|
"name" VARCHAR(100) NOT NULL,
|
||||||
create index session_created_at_idx on session(created_at);
|
"domain" VARCHAR(500),
|
||||||
create index session_website_id_idx on session(website_id);
|
"share_id" VARCHAR(64),
|
||||||
|
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||||
create index pageview_created_at_idx on pageview(created_at);
|
|
||||||
create index pageview_website_id_idx on pageview(website_id);
|
PRIMARY KEY ("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);
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "account.username_unique" ON "account"("username");
|
||||||
create index event_created_at_idx on event(created_at);
|
|
||||||
create index event_website_id_idx on event(website_id);
|
-- CreateIndex
|
||||||
create index event_session_id_idx on event(session_id);
|
CREATE INDEX "event_created_at_idx" ON "event"("created_at");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "event_session_id_idx" ON "event"("session_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "event_website_id_idx" ON "event"("website_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "pageview_created_at_idx" ON "pageview"("created_at");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "pageview_session_id_idx" ON "pageview"("session_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "pageview_website_id_created_at_idx" ON "pageview"("website_id", "created_at");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "pageview_website_id_idx" ON "pageview"("website_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "pageview_website_id_session_id_created_at_idx" ON "pageview"("website_id", "session_id", "created_at");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "session.session_uuid_unique" ON "session"("session_uuid");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "session_created_at_idx" ON "session"("created_at");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "session_website_id_idx" ON "session"("website_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "website.website_uuid_unique" ON "website"("website_uuid");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "website.share_id_unique" ON "website"("share_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "website_user_id_idx" ON "website"("user_id");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "event" ADD FOREIGN KEY ("session_id") REFERENCES "session"("session_id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "event" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "pageview" ADD FOREIGN KEY ("session_id") REFERENCES "session"("session_id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "pageview" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "session" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "website" ADD FOREIGN KEY ("user_id") REFERENCES "account"("user_id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
insert into account (username, password, is_admin) values ('admin', '${bcrypt.hashSync(
|
insert into account (username, password, is_admin) values ('admin', '${bcrypt.hashSync(
|
||||||
umamiAdminPassword,
|
umamiAdminPassword,
|
||||||
10
|
10
|
||||||
@@ -1116,7 +1181,6 @@ async function startUmamiService(request: FastifyRequest<ServiceStartStop>) {
|
|||||||
},
|
},
|
||||||
volumes: volumeMounts
|
volumes: volumeMounts
|
||||||
};
|
};
|
||||||
console.log(composeFile)
|
|
||||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||||
await startServiceContainers(destinationDocker.id, composeFileDestination)
|
await startServiceContainers(destinationDocker.id, composeFileDestination)
|
||||||
@@ -1318,10 +1382,6 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
|
|||||||
const teamId = request.user.teamId;
|
const teamId = request.user.teamId;
|
||||||
const { version, fqdn, destinationDocker, secrets, exposePort, network, port, workdir, image, appwrite } = await defaultServiceConfigurations({ id, teamId })
|
const { version, fqdn, destinationDocker, secrets, exposePort, network, port, workdir, image, appwrite } = await defaultServiceConfigurations({ id, teamId })
|
||||||
|
|
||||||
let isStatsEnabled = false
|
|
||||||
if (secrets.find(s => s === '_APP_USAGE_STATS=enabled')) {
|
|
||||||
isStatsEnabled = true
|
|
||||||
}
|
|
||||||
const {
|
const {
|
||||||
opensslKeyV1,
|
opensslKeyV1,
|
||||||
executorSecret,
|
executorSecret,
|
||||||
@@ -1699,50 +1759,48 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
if (isStatsEnabled) {
|
dockerCompose[id].depends_on.push(`${id}-influxdb`);
|
||||||
dockerCompose[id].depends_on.push(`${id}-influxdb`);
|
dockerCompose[`${id}-usage`] = {
|
||||||
dockerCompose[`${id}-usage`] = {
|
image: `${image}:${version}`,
|
||||||
image: `${image}:${version}`,
|
container_name: `${id}-usage`,
|
||||||
container_name: `${id}-usage`,
|
labels: makeLabelForServices('appwrite'),
|
||||||
labels: makeLabelForServices('appwrite'),
|
entrypoint: "usage",
|
||||||
entrypoint: "usage",
|
depends_on: [
|
||||||
depends_on: [
|
`${id}-mariadb`,
|
||||||
`${id}-mariadb`,
|
`${id}-influxdb`,
|
||||||
`${id}-influxdb`,
|
],
|
||||||
],
|
environment: [
|
||||||
environment: [
|
"_APP_ENV=production",
|
||||||
"_APP_ENV=production",
|
`_APP_OPENSSL_KEY_V1=${opensslKeyV1}`,
|
||||||
`_APP_OPENSSL_KEY_V1=${opensslKeyV1}`,
|
`_APP_DB_HOST=${mariadbHost}`,
|
||||||
`_APP_DB_HOST=${mariadbHost}`,
|
`_APP_DB_PORT=${mariadbPort}`,
|
||||||
`_APP_DB_PORT=${mariadbPort}`,
|
`_APP_DB_SCHEMA=${mariadbDatabase}`,
|
||||||
`_APP_DB_SCHEMA=${mariadbDatabase}`,
|
`_APP_DB_USER=${mariadbUser}`,
|
||||||
`_APP_DB_USER=${mariadbUser}`,
|
`_APP_DB_PASS=${mariadbPassword}`,
|
||||||
`_APP_DB_PASS=${mariadbPassword}`,
|
`_APP_INFLUXDB_HOST=${id}-influxdb`,
|
||||||
`_APP_INFLUXDB_HOST=${id}-influxdb`,
|
"_APP_INFLUXDB_PORT=8086",
|
||||||
"_APP_INFLUXDB_PORT=8086",
|
`_APP_REDIS_HOST=${id}-redis`,
|
||||||
`_APP_REDIS_HOST=${id}-redis`,
|
"_APP_REDIS_PORT=6379",
|
||||||
"_APP_REDIS_PORT=6379",
|
...secrets
|
||||||
...secrets
|
],
|
||||||
],
|
...defaultComposeConfiguration(network),
|
||||||
...defaultComposeConfiguration(network),
|
}
|
||||||
}
|
dockerCompose[`${id}-influxdb`] = {
|
||||||
dockerCompose[`${id}-influxdb`] = {
|
image: "appwrite/influxdb:1.5.0",
|
||||||
image: "appwrite/influxdb:1.5.0",
|
container_name: `${id}-influxdb`,
|
||||||
container_name: `${id}-influxdb`,
|
volumes: [
|
||||||
volumes: [
|
`${id}-influxdb:/var/lib/influxdb:rw`
|
||||||
`${id}-influxdb:/var/lib/influxdb:rw`
|
],
|
||||||
],
|
...defaultComposeConfiguration(network),
|
||||||
...defaultComposeConfiguration(network),
|
}
|
||||||
}
|
dockerCompose[`${id}-telegraf`] = {
|
||||||
dockerCompose[`${id}-telegraf`] = {
|
image: "appwrite/telegraf:1.4.0",
|
||||||
image: "appwrite/telegraf:1.4.0",
|
container_name: `${id}-telegraf`,
|
||||||
container_name: `${id}-telegraf`,
|
environment: [
|
||||||
environment: [
|
`_APP_INFLUXDB_HOST=${id}-influxdb`,
|
||||||
`_APP_INFLUXDB_HOST=${id}-influxdb`,
|
"_APP_INFLUXDB_PORT=8086",
|
||||||
"_APP_INFLUXDB_PORT=8086",
|
],
|
||||||
],
|
...defaultComposeConfiguration(network),
|
||||||
...defaultComposeConfiguration(network),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const composeFile: any = {
|
const composeFile: any = {
|
||||||
@@ -1979,8 +2037,8 @@ async function startGlitchTipService(request: FastifyRequest<ServiceStartStop>)
|
|||||||
EMAIL_PORT: emailSmtpPort,
|
EMAIL_PORT: emailSmtpPort,
|
||||||
EMAIL_HOST_USER: emailSmtpUser,
|
EMAIL_HOST_USER: emailSmtpUser,
|
||||||
EMAIL_HOST_PASSWORD: emailSmtpPassword,
|
EMAIL_HOST_PASSWORD: emailSmtpPassword,
|
||||||
EMAIL_USE_TLS: emailSmtpUseTls,
|
EMAIL_USE_TLS: emailSmtpUseTls ? 'True' : 'False',
|
||||||
EMAIL_USE_SSL: emailSmtpUseSsl,
|
EMAIL_USE_SSL: emailSmtpUseSsl ? 'True' : 'False',
|
||||||
EMAIL_BACKEND: emailBackend,
|
EMAIL_BACKEND: emailBackend,
|
||||||
MAILGUN_API_KEY: mailgunApiKey,
|
MAILGUN_API_KEY: mailgunApiKey,
|
||||||
SENDGRID_API_KEY: sendgridApiKey,
|
SENDGRID_API_KEY: sendgridApiKey,
|
||||||
@@ -2590,3 +2648,132 @@ async function startTaigaService(request: FastifyRequest<ServiceStartStop>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function startGrafanaService(request: FastifyRequest<ServiceStartStop>) {
|
||||||
|
try {
|
||||||
|
const { id } = request.params;
|
||||||
|
const teamId = request.user.teamId;
|
||||||
|
const service = await getServiceFromDB({ id, teamId });
|
||||||
|
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } =
|
||||||
|
service;
|
||||||
|
const network = destinationDockerId && destinationDocker.network;
|
||||||
|
const port = getServiceMainPort('grafana');
|
||||||
|
|
||||||
|
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||||
|
const image = getServiceImage(type);
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
grafana: {
|
||||||
|
image: `${image}:${version}`,
|
||||||
|
volumes: [`${id}-grafana:/var/lib/grafana`],
|
||||||
|
environmentVariables: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (serviceSecret.length > 0) {
|
||||||
|
serviceSecret.forEach((secret) => {
|
||||||
|
config.grafana.environmentVariables[secret.name] = secret.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const { volumeMounts } = persistentVolumes(id, persistentStorage, config)
|
||||||
|
const composeFile: ComposeFile = {
|
||||||
|
version: '3.8',
|
||||||
|
services: {
|
||||||
|
[id]: {
|
||||||
|
container_name: id,
|
||||||
|
image: config.grafana.image,
|
||||||
|
volumes: config.grafana.volumes,
|
||||||
|
environment: config.grafana.environmentVariables,
|
||||||
|
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
|
||||||
|
labels: makeLabelForServices('grafana'),
|
||||||
|
...defaultComposeConfiguration(network),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
networks: {
|
||||||
|
[network]: {
|
||||||
|
external: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
volumes: volumeMounts
|
||||||
|
};
|
||||||
|
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||||
|
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||||
|
await startServiceContainers(destinationDocker.id, composeFileDestination)
|
||||||
|
return {}
|
||||||
|
} catch ({ status, message }) {
|
||||||
|
return errorHandler({ status, message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function startTriliumService(request: FastifyRequest<ServiceStartStop>) {
|
||||||
|
try {
|
||||||
|
const { id } = request.params;
|
||||||
|
const teamId = request.user.teamId;
|
||||||
|
const service = await getServiceFromDB({ id, teamId });
|
||||||
|
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } =
|
||||||
|
service;
|
||||||
|
const network = destinationDockerId && destinationDocker.network;
|
||||||
|
const port = getServiceMainPort('trilium');
|
||||||
|
|
||||||
|
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||||
|
const image = getServiceImage(type);
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
trilium: {
|
||||||
|
image: `${image}:${version}`,
|
||||||
|
volumes: [`${id}-trilium:/home/node/trilium-data`],
|
||||||
|
environmentVariables: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (serviceSecret.length > 0) {
|
||||||
|
serviceSecret.forEach((secret) => {
|
||||||
|
config.trilium.environmentVariables[secret.name] = secret.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const { volumeMounts } = persistentVolumes(id, persistentStorage, config)
|
||||||
|
const composeFile: ComposeFile = {
|
||||||
|
version: '3.8',
|
||||||
|
services: {
|
||||||
|
[id]: {
|
||||||
|
container_name: id,
|
||||||
|
image: config.trilium.image,
|
||||||
|
volumes: config.trilium.volumes,
|
||||||
|
environment: config.trilium.environmentVariables,
|
||||||
|
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
|
||||||
|
labels: makeLabelForServices('trilium'),
|
||||||
|
...defaultComposeConfiguration(network),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
networks: {
|
||||||
|
[network]: {
|
||||||
|
external: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
volumes: volumeMounts
|
||||||
|
};
|
||||||
|
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||||
|
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||||
|
await startServiceContainers(destinationDocker.id, composeFileDestination)
|
||||||
|
return {}
|
||||||
|
} catch ({ status, message }) {
|
||||||
|
return errorHandler({ status, message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function migrateAppwriteDB(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
|
||||||
|
try {
|
||||||
|
const { id } = request.params
|
||||||
|
const teamId = request.user.teamId;
|
||||||
|
const {
|
||||||
|
destinationDockerId,
|
||||||
|
destinationDocker,
|
||||||
|
} = await getServiceFromDB({ id, teamId });
|
||||||
|
if (destinationDockerId) {
|
||||||
|
await executeDockerCmd({
|
||||||
|
dockerId: destinationDocker.id,
|
||||||
|
command: `docker exec ${id} migrate`
|
||||||
|
})
|
||||||
|
return await reply.code(201).send()
|
||||||
|
}
|
||||||
|
throw { status: 500, message: 'Could cleanup logs.' }
|
||||||
|
} catch ({ status, message }) {
|
||||||
|
return errorHandler({ status, message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,24 @@
|
|||||||
|
/*
|
||||||
|
Example of a supported version:
|
||||||
|
{
|
||||||
|
// Name used to identify the service internally
|
||||||
|
name: 'umami',
|
||||||
|
// Fancier name to show to the user
|
||||||
|
fancyName: 'Umami',
|
||||||
|
// Docker base image for the service
|
||||||
|
baseImage: 'ghcr.io/mikecao/umami',
|
||||||
|
// Optional: If there is any dependent image, you should list it here
|
||||||
|
images: [],
|
||||||
|
// Usable tags
|
||||||
|
versions: ['postgresql-latest'],
|
||||||
|
// Which tag is the recommended
|
||||||
|
recommendedVersion: 'postgresql-latest',
|
||||||
|
// Application's default port, Umami listens on 3000
|
||||||
|
ports: {
|
||||||
|
main: 3000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
export const supportedServiceTypesAndVersions = [
|
export const supportedServiceTypesAndVersions = [
|
||||||
{
|
{
|
||||||
name: 'plausibleanalytics',
|
name: 'plausibleanalytics',
|
||||||
@@ -116,7 +137,7 @@ export const supportedServiceTypesAndVersions = [
|
|||||||
{
|
{
|
||||||
name: 'umami',
|
name: 'umami',
|
||||||
fancyName: 'Umami',
|
fancyName: 'Umami',
|
||||||
baseImage: 'ghcr.io/mikecao/umami',
|
baseImage: 'ghcr.io/umami-software/umami',
|
||||||
images: ['postgres:12-alpine'],
|
images: ['postgres:12-alpine'],
|
||||||
versions: ['postgresql-latest'],
|
versions: ['postgresql-latest'],
|
||||||
recommendedVersion: 'postgresql-latest',
|
recommendedVersion: 'postgresql-latest',
|
||||||
@@ -151,8 +172,8 @@ export const supportedServiceTypesAndVersions = [
|
|||||||
fancyName: 'Appwrite',
|
fancyName: 'Appwrite',
|
||||||
baseImage: 'appwrite/appwrite',
|
baseImage: 'appwrite/appwrite',
|
||||||
images: ['mariadb:10.7', 'redis:6.2-alpine', 'appwrite/telegraf:1.4.0'],
|
images: ['mariadb:10.7', 'redis:6.2-alpine', 'appwrite/telegraf:1.4.0'],
|
||||||
versions: ['latest', '0.15.3'],
|
versions: ['latest', '1.0', '0.15.3'],
|
||||||
recommendedVersion: '0.15.3',
|
recommendedVersion: '1.0',
|
||||||
ports: {
|
ports: {
|
||||||
main: 80
|
main: 80
|
||||||
}
|
}
|
||||||
@@ -212,4 +233,26 @@ export const supportedServiceTypesAndVersions = [
|
|||||||
// main: 80
|
// main: 80
|
||||||
// }
|
// }
|
||||||
// },
|
// },
|
||||||
|
{
|
||||||
|
name: 'grafana',
|
||||||
|
fancyName: 'Grafana Dashboard',
|
||||||
|
baseImage: 'grafana/grafana',
|
||||||
|
images: [],
|
||||||
|
versions: ['latest', '9.1.3', '9.1.2', '9.0.8', '8.3.11', '8.4.11', '8.5.11'],
|
||||||
|
recommendedVersion: 'latest',
|
||||||
|
ports: {
|
||||||
|
main: 3000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'trilium',
|
||||||
|
fancyName: 'Trilium Notes',
|
||||||
|
baseImage: 'zadam/trilium',
|
||||||
|
images: [],
|
||||||
|
versions: ['latest'],
|
||||||
|
recommendedVersion: 'latest',
|
||||||
|
ports: {
|
||||||
|
main: 8080
|
||||||
|
}
|
||||||
|
},
|
||||||
];
|
];
|
||||||
@@ -5,6 +5,7 @@ import axios from 'axios';
|
|||||||
import { FastifyReply } from 'fastify';
|
import { FastifyReply } from 'fastify';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
|
import csv from 'csvtojson';
|
||||||
|
|
||||||
import { day } from '../../../../lib/dayjs';
|
import { day } from '../../../../lib/dayjs';
|
||||||
import { makeLabelForStandaloneApplication, setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common';
|
import { makeLabelForStandaloneApplication, setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common';
|
||||||
@@ -12,7 +13,7 @@ import { checkDomainsIsValidInDNS, checkDoubleBranch, checkExposedPort, createDi
|
|||||||
import { checkContainer, formatLabelsOnDocker, isContainerExited, removeContainer } from '../../../../lib/docker';
|
import { checkContainer, formatLabelsOnDocker, isContainerExited, removeContainer } from '../../../../lib/docker';
|
||||||
|
|
||||||
import type { FastifyRequest } from 'fastify';
|
import type { FastifyRequest } from 'fastify';
|
||||||
import type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication } from './types';
|
import type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication, RestartPreviewApplication, GetBuilds } from './types';
|
||||||
import { OnlyId } from '../../../../types';
|
import { OnlyId } from '../../../../types';
|
||||||
|
|
||||||
function filterObject(obj, callback) {
|
function filterObject(obj, callback) {
|
||||||
@@ -74,14 +75,19 @@ export async function getApplicationStatus(request: FastifyRequest<OnlyId>) {
|
|||||||
const { teamId } = request.user
|
const { teamId } = request.user
|
||||||
let isRunning = false;
|
let isRunning = false;
|
||||||
let isExited = false;
|
let isExited = false;
|
||||||
|
let isRestarting = false;
|
||||||
const application: any = await getApplicationFromDB(id, teamId);
|
const application: any = await getApplicationFromDB(id, teamId);
|
||||||
if (application?.destinationDockerId) {
|
if (application?.destinationDockerId) {
|
||||||
isRunning = await checkContainer({ dockerId: application.destinationDocker.id, container: id });
|
const status = await checkContainer({ dockerId: application.destinationDocker.id, container: id });
|
||||||
isExited = await isContainerExited(application.destinationDocker.id, id);
|
if (status?.found) {
|
||||||
|
isRunning = status.status.isRunning;
|
||||||
|
isExited = status.status.isExited;
|
||||||
|
isRestarting = status.status.isRestarting
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
isRunning,
|
isRunning,
|
||||||
|
isRestarting,
|
||||||
isExited,
|
isExited,
|
||||||
};
|
};
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
@@ -157,7 +163,8 @@ export async function getApplicationFromDB(id: string, teamId: string) {
|
|||||||
gitSource: { include: { githubApp: true, gitlabApp: true } },
|
gitSource: { include: { githubApp: true, gitlabApp: true } },
|
||||||
secrets: true,
|
secrets: true,
|
||||||
persistentStorage: true,
|
persistentStorage: true,
|
||||||
connectedDatabase: true
|
connectedDatabase: true,
|
||||||
|
previewApplication: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!application) {
|
if (!application) {
|
||||||
@@ -252,8 +259,8 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
|
|||||||
exposePort = Number(exposePort);
|
exposePort = Number(exposePort);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { destinationDocker: { id: dockerId, remoteIpAddress }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } })
|
const { destinationDocker: { engine, remoteEngine, remoteIpAddress }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } })
|
||||||
if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, dockerId, remoteIpAddress })
|
if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress })
|
||||||
if (denoOptions) denoOptions = denoOptions.trim();
|
if (denoOptions) denoOptions = denoOptions.trim();
|
||||||
const defaultConfiguration = await setDefaultConfiguration({
|
const defaultConfiguration = await setDefaultConfiguration({
|
||||||
buildPack,
|
buildPack,
|
||||||
@@ -313,15 +320,10 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
|
|||||||
export async function saveApplicationSettings(request: FastifyRequest<SaveApplicationSettings>, reply: FastifyReply) {
|
export async function saveApplicationSettings(request: FastifyRequest<SaveApplicationSettings>, reply: FastifyReply) {
|
||||||
try {
|
try {
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
const { debug, previews, dualCerts, autodeploy, branch, projectId, isBot, isDBBranching } = request.body
|
const { debug, previews, dualCerts, autodeploy, branch, projectId, isBot, isDBBranching, isCustomSSL } = request.body
|
||||||
// const isDouble = await checkDoubleBranch(branch, projectId);
|
|
||||||
// if (isDouble && autodeploy) {
|
|
||||||
// await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false } })
|
|
||||||
// throw { status: 500, message: 'Cannot activate automatic deployments until only one application is defined for this repository / branch.' }
|
|
||||||
// }
|
|
||||||
await prisma.application.update({
|
await prisma.application.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: { fqdn: isBot ? null : undefined, settings: { update: { debug, previews, dualCerts, autodeploy, isBot, isDBBranching } } },
|
data: { fqdn: isBot ? null : undefined, settings: { update: { debug, previews, dualCerts, autodeploy, isBot, isDBBranching, isCustomSSL } } },
|
||||||
include: { destinationDocker: true }
|
include: { destinationDocker: true }
|
||||||
});
|
});
|
||||||
return reply.code(201).send();
|
return reply.code(201).send();
|
||||||
@@ -339,10 +341,11 @@ export async function stopPreviewApplication(request: FastifyRequest<StopPreview
|
|||||||
if (application?.destinationDockerId) {
|
if (application?.destinationDockerId) {
|
||||||
const container = `${id}-${pullmergeRequestId}`
|
const container = `${id}-${pullmergeRequestId}`
|
||||||
const { id: dockerId } = application.destinationDocker;
|
const { id: dockerId } = application.destinationDocker;
|
||||||
const found = await checkContainer({ dockerId, container });
|
const { found } = await checkContainer({ dockerId, container });
|
||||||
if (found) {
|
if (found) {
|
||||||
await removeContainer({ id: container, dockerId: application.destinationDocker.id });
|
await removeContainer({ id: container, dockerId: application.destinationDocker.id });
|
||||||
}
|
}
|
||||||
|
await prisma.previewApplication.deleteMany({ where: { applicationId: application.id, pullmergeRequestId } })
|
||||||
}
|
}
|
||||||
return reply.code(201).send();
|
return reply.code(201).send();
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
@@ -366,7 +369,10 @@ export async function restartApplication(request: FastifyRequest<OnlyId>, reply:
|
|||||||
if (secrets.length > 0) {
|
if (secrets.length > 0) {
|
||||||
secrets.forEach((secret) => {
|
secrets.forEach((secret) => {
|
||||||
if (pullmergeRequestId) {
|
if (pullmergeRequestId) {
|
||||||
if (secret.isPRMRSecret) {
|
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
|
||||||
|
if (isSecretFound.length > 0) {
|
||||||
|
envs.push(`${secret.name}=${isSecretFound[0].value}`);
|
||||||
|
} else {
|
||||||
envs.push(`${secret.name}=${secret.value}`);
|
envs.push(`${secret.name}=${secret.value}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -463,7 +469,7 @@ export async function stopApplication(request: FastifyRequest<OnlyId>, reply: Fa
|
|||||||
const application: any = await getApplicationFromDB(id, teamId);
|
const application: any = await getApplicationFromDB(id, teamId);
|
||||||
if (application?.destinationDockerId) {
|
if (application?.destinationDockerId) {
|
||||||
const { id: dockerId } = application.destinationDocker;
|
const { id: dockerId } = application.destinationDocker;
|
||||||
const found = await checkContainer({ dockerId, container: id });
|
const { found } = await checkContainer({ dockerId, container: id });
|
||||||
if (found) {
|
if (found) {
|
||||||
await removeContainer({ id, dockerId: application.destinationDocker.id });
|
await removeContainer({ id, dockerId: application.destinationDocker.id });
|
||||||
}
|
}
|
||||||
@@ -534,14 +540,14 @@ export async function checkDNS(request: FastifyRequest<CheckDNS>) {
|
|||||||
}
|
}
|
||||||
if (exposePort) exposePort = Number(exposePort);
|
if (exposePort) exposePort = Number(exposePort);
|
||||||
|
|
||||||
const { destinationDocker: { id: dockerId, remoteIpAddress, remoteEngine }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } })
|
const { destinationDocker: { engine, remoteIpAddress, remoteEngine }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } })
|
||||||
const { isDNSCheckEnabled } = await prisma.setting.findFirst({});
|
const { isDNSCheckEnabled } = await prisma.setting.findFirst({});
|
||||||
|
|
||||||
const found = await isDomainConfigured({ id, fqdn, remoteIpAddress });
|
const found = await isDomainConfigured({ id, fqdn, remoteIpAddress });
|
||||||
if (found) {
|
if (found) {
|
||||||
throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` }
|
throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` }
|
||||||
}
|
}
|
||||||
if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, dockerId, remoteIpAddress })
|
if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress })
|
||||||
if (isDNSCheckEnabled && !isDev && !forceSave) {
|
if (isDNSCheckEnabled && !isDev && !forceSave) {
|
||||||
let hostname = request.hostname.split(':')[0];
|
let hostname = request.hostname.split(':')[0];
|
||||||
if (remoteEngine) hostname = remoteIpAddress;
|
if (remoteEngine) hostname = remoteIpAddress;
|
||||||
@@ -607,7 +613,7 @@ export async function deployApplication(request: FastifyRequest<DeployApplicatio
|
|||||||
githubAppId: application.gitSource?.githubApp?.id,
|
githubAppId: application.gitSource?.githubApp?.id,
|
||||||
gitlabAppId: application.gitSource?.gitlabApp?.id,
|
gitlabAppId: application.gitSource?.gitlabApp?.id,
|
||||||
status: 'queued',
|
status: 'queued',
|
||||||
type: 'manual'
|
type: pullmergeRequestId ? application.gitSource?.githubApp?.id ? 'manual_pr' : 'manual_mr' : 'manual'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@@ -775,55 +781,79 @@ export async function saveConnectedDatabase(request, reply) {
|
|||||||
export async function getSecrets(request: FastifyRequest<OnlyId>) {
|
export async function getSecrets(request: FastifyRequest<OnlyId>) {
|
||||||
try {
|
try {
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
|
|
||||||
let secrets = await prisma.secret.findMany({
|
let secrets = await prisma.secret.findMany({
|
||||||
where: { applicationId: id },
|
where: { applicationId: id, isPRMRSecret: false },
|
||||||
orderBy: { createdAt: 'desc' }
|
orderBy: { createdAt: 'asc' }
|
||||||
});
|
});
|
||||||
|
let previewSecrets = await prisma.secret.findMany({
|
||||||
|
where: { applicationId: id, isPRMRSecret: true },
|
||||||
|
orderBy: { createdAt: 'asc' }
|
||||||
|
});
|
||||||
|
|
||||||
secrets = secrets.map((secret) => {
|
secrets = secrets.map((secret) => {
|
||||||
secret.value = decrypt(secret.value);
|
secret.value = decrypt(secret.value);
|
||||||
return secret;
|
return secret;
|
||||||
});
|
});
|
||||||
secrets = secrets.filter((secret) => !secret.isPRMRSecret).sort((a, b) => {
|
previewSecrets = previewSecrets.map((secret) => {
|
||||||
return ('' + a.name).localeCompare(b.name);
|
secret.value = decrypt(secret.value);
|
||||||
})
|
return secret;
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
secrets
|
previewSecrets: previewSecrets.sort((a, b) => {
|
||||||
|
return ('' + a.name).localeCompare(b.name);
|
||||||
|
}),
|
||||||
|
secrets: secrets.sort((a, b) => {
|
||||||
|
return ('' + a.name).localeCompare(b.name);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updatePreviewSecret(request: FastifyRequest<SaveSecret>, reply: FastifyReply) {
|
||||||
|
try {
|
||||||
|
const { id } = request.params
|
||||||
|
let { name, value } = request.body
|
||||||
|
if (value) {
|
||||||
|
value = encrypt(value.trim())
|
||||||
|
} else {
|
||||||
|
value = ''
|
||||||
|
}
|
||||||
|
await prisma.secret.updateMany({
|
||||||
|
where: { applicationId: id, name, isPRMRSecret: true },
|
||||||
|
data: { value }
|
||||||
|
});
|
||||||
|
return reply.code(201).send()
|
||||||
|
} catch ({ status, message }) {
|
||||||
|
return errorHandler({ status, message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function updateSecret(request: FastifyRequest<SaveSecret>, reply: FastifyReply) {
|
||||||
|
try {
|
||||||
|
const { id } = request.params
|
||||||
|
const { name, value, isBuildSecret = undefined } = request.body
|
||||||
|
await prisma.secret.updateMany({
|
||||||
|
where: { applicationId: id, name },
|
||||||
|
data: { value: encrypt(value.trim()), isBuildSecret }
|
||||||
|
});
|
||||||
|
return reply.code(201).send()
|
||||||
|
} catch ({ status, message }) {
|
||||||
|
return errorHandler({ status, message })
|
||||||
|
}
|
||||||
|
}
|
||||||
export async function saveSecret(request: FastifyRequest<SaveSecret>, reply: FastifyReply) {
|
export async function saveSecret(request: FastifyRequest<SaveSecret>, reply: FastifyReply) {
|
||||||
try {
|
try {
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
let { name, value, isBuildSecret, isPRMRSecret, isNew } = request.body
|
const { name, value, isBuildSecret = false } = request.body
|
||||||
|
await prisma.secret.create({
|
||||||
if (isNew) {
|
data: { name, value: encrypt(value.trim()), isBuildSecret, isPRMRSecret: false, application: { connect: { id } } }
|
||||||
const found = await prisma.secret.findFirst({ where: { name, applicationId: id, isPRMRSecret } });
|
});
|
||||||
if (found) {
|
await prisma.secret.create({
|
||||||
throw { status: 500, message: `Secret ${name} already exists.` }
|
data: { name, value: encrypt(value.trim()), isBuildSecret, isPRMRSecret: true, application: { connect: { id } } }
|
||||||
} else {
|
});
|
||||||
value = encrypt(value.trim());
|
|
||||||
await prisma.secret.create({
|
|
||||||
data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
value = encrypt(value.trim());
|
|
||||||
const found = await prisma.secret.findFirst({ where: { applicationId: id, name, isPRMRSecret } });
|
|
||||||
|
|
||||||
if (found) {
|
|
||||||
await prisma.secret.updateMany({
|
|
||||||
where: { applicationId: id, name, isPRMRSecret },
|
|
||||||
data: { value, isBuildSecret, isPRMRSecret }
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await prisma.secret.create({
|
|
||||||
data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return reply.code(201).send()
|
return reply.code(201).send()
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
@@ -884,6 +914,181 @@ export async function deleteStorage(request: FastifyRequest<DeleteStorage>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function restartPreview(request: FastifyRequest<RestartPreviewApplication>, reply: FastifyReply) {
|
||||||
|
try {
|
||||||
|
const { id, pullmergeRequestId } = request.params
|
||||||
|
const { teamId } = request.user
|
||||||
|
let application: any = await getApplicationFromDB(id, teamId);
|
||||||
|
if (application?.destinationDockerId) {
|
||||||
|
const buildId = cuid();
|
||||||
|
const { id: dockerId, network } = application.destinationDocker;
|
||||||
|
const { secrets, port, repository, persistentStorage, id: applicationId, buildPack, exposePort } = application;
|
||||||
|
|
||||||
|
const envs = [
|
||||||
|
`PORT=${port}`
|
||||||
|
];
|
||||||
|
if (secrets.length > 0) {
|
||||||
|
secrets.forEach((secret) => {
|
||||||
|
if (pullmergeRequestId) {
|
||||||
|
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
|
||||||
|
if (isSecretFound.length > 0) {
|
||||||
|
envs.push(`${secret.name}=${isSecretFound[0].value}`);
|
||||||
|
} else {
|
||||||
|
envs.push(`${secret.name}=${secret.value}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!secret.isPRMRSecret) {
|
||||||
|
envs.push(`${secret.name}=${secret.value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const { workdir } = await createDirectories({ repository, buildId });
|
||||||
|
const labels = []
|
||||||
|
let image = null
|
||||||
|
const { stdout: container } = await executeDockerCmd({ dockerId, command: `docker container ls --filter 'label=com.docker.compose.service=${id}-${pullmergeRequestId}' --format '{{json .}}'` })
|
||||||
|
const containersArray = container.trim().split('\n');
|
||||||
|
for (const container of containersArray) {
|
||||||
|
const containerObj = formatLabelsOnDocker(container);
|
||||||
|
image = containerObj[0].Image
|
||||||
|
Object.keys(containerObj[0].Labels).forEach(function (key) {
|
||||||
|
if (key.startsWith('coolify')) {
|
||||||
|
labels.push(`${key}=${containerObj[0].Labels[key]}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
let imageFound = false;
|
||||||
|
try {
|
||||||
|
await executeDockerCmd({
|
||||||
|
dockerId,
|
||||||
|
command: `docker image inspect ${image}`
|
||||||
|
})
|
||||||
|
imageFound = true;
|
||||||
|
} catch (error) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
if (!imageFound) {
|
||||||
|
throw { status: 500, message: 'Image not found, cannot restart application.' }
|
||||||
|
}
|
||||||
|
await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
|
||||||
|
|
||||||
|
let envFound = false;
|
||||||
|
try {
|
||||||
|
envFound = !!(await fs.stat(`${workdir}/.env`));
|
||||||
|
} catch (error) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
const volumes =
|
||||||
|
persistentStorage?.map((storage) => {
|
||||||
|
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${buildPack !== 'docker' ? '/app' : ''
|
||||||
|
}${storage.path}`;
|
||||||
|
}) || [];
|
||||||
|
const composeVolumes = volumes.map((volume) => {
|
||||||
|
return {
|
||||||
|
[`${volume.split(':')[0]}`]: {
|
||||||
|
name: volume.split(':')[0]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const composeFile = {
|
||||||
|
version: '3.8',
|
||||||
|
services: {
|
||||||
|
[`${applicationId}-${pullmergeRequestId}`]: {
|
||||||
|
image,
|
||||||
|
container_name: `${applicationId}-${pullmergeRequestId}`,
|
||||||
|
volumes,
|
||||||
|
env_file: envFound ? [`${workdir}/.env`] : [],
|
||||||
|
labels,
|
||||||
|
depends_on: [],
|
||||||
|
expose: [port],
|
||||||
|
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
|
||||||
|
...defaultComposeConfiguration(network),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
networks: {
|
||||||
|
[network]: {
|
||||||
|
external: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
volumes: Object.assign({}, ...composeVolumes)
|
||||||
|
};
|
||||||
|
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
|
||||||
|
await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}-${pullmergeRequestId}` })
|
||||||
|
await executeDockerCmd({ dockerId, command: `docker rm ${id}-${pullmergeRequestId}` })
|
||||||
|
await executeDockerCmd({ dockerId, command: `docker compose --project-directory ${workdir} up -d` })
|
||||||
|
return reply.code(201).send();
|
||||||
|
}
|
||||||
|
throw { status: 500, message: 'Application cannot be restarted.' }
|
||||||
|
} catch ({ status, message }) {
|
||||||
|
return errorHandler({ status, message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function getPreviewStatus(request: FastifyRequest<RestartPreviewApplication>) {
|
||||||
|
try {
|
||||||
|
const { id, pullmergeRequestId } = request.params
|
||||||
|
const { teamId } = request.user
|
||||||
|
let isRunning = false;
|
||||||
|
let isExited = false;
|
||||||
|
let isRestarting = false;
|
||||||
|
let isBuilding = false
|
||||||
|
const application: any = await getApplicationFromDB(id, teamId);
|
||||||
|
if (application?.destinationDockerId) {
|
||||||
|
const status = await checkContainer({ dockerId: application.destinationDocker.id, container: `${id}-${pullmergeRequestId}` });
|
||||||
|
if (status?.found) {
|
||||||
|
isRunning = status.status.isRunning;
|
||||||
|
isExited = status.status.isExited;
|
||||||
|
isRestarting = status.status.isRestarting
|
||||||
|
}
|
||||||
|
const building = await prisma.build.findMany({ where: { applicationId: id, pullmergeRequestId, status: { in: ['queued', 'running'] } } })
|
||||||
|
isBuilding = building.length > 0
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
isBuilding,
|
||||||
|
isRunning,
|
||||||
|
isRestarting,
|
||||||
|
isExited,
|
||||||
|
};
|
||||||
|
} catch ({ status, message }) {
|
||||||
|
return errorHandler({ status, message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function loadPreviews(request: FastifyRequest<OnlyId>) {
|
||||||
|
try {
|
||||||
|
const { id } = request.params
|
||||||
|
const application = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } });
|
||||||
|
const { stdout } = await executeDockerCmd({ dockerId: application.destinationDocker.id, command: `docker container ls --filter 'name=${id}-' --format "{{json .}}"` })
|
||||||
|
if (stdout === '') {
|
||||||
|
throw { status: 500, message: 'No previews found.' }
|
||||||
|
}
|
||||||
|
const containers = formatLabelsOnDocker(stdout).filter(container => container.Labels['coolify.configuration'] && container.Labels['coolify.type'] === 'standalone-application')
|
||||||
|
|
||||||
|
const jsonContainers = containers
|
||||||
|
.map((container) =>
|
||||||
|
JSON.parse(Buffer.from(container.Labels['coolify.configuration'], 'base64').toString())
|
||||||
|
)
|
||||||
|
.filter((container) => {
|
||||||
|
return container.pullmergeRequestId && container.applicationId === id;
|
||||||
|
});
|
||||||
|
for (const container of jsonContainers) {
|
||||||
|
const found = await prisma.previewApplication.findMany({ where: { applicationId: container.applicationId, pullmergeRequestId: container.pullmergeRequestId } })
|
||||||
|
if (found.length === 0) {
|
||||||
|
await prisma.previewApplication.create({
|
||||||
|
data: {
|
||||||
|
pullmergeRequestId: container.pullmergeRequestId,
|
||||||
|
sourceBranch: container.branch,
|
||||||
|
customDomain: container.fqdn,
|
||||||
|
application: { connect: { id: container.applicationId } }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
previews: await prisma.previewApplication.findMany({ where: { applicationId: id } })
|
||||||
|
}
|
||||||
|
} catch ({ status, message }) {
|
||||||
|
return errorHandler({ status, message })
|
||||||
|
}
|
||||||
|
}
|
||||||
export async function getPreviews(request: FastifyRequest<OnlyId>) {
|
export async function getPreviews(request: FastifyRequest<OnlyId>) {
|
||||||
try {
|
try {
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
@@ -899,26 +1104,7 @@ export async function getPreviews(request: FastifyRequest<OnlyId>) {
|
|||||||
|
|
||||||
const applicationSecrets = secrets.filter((secret) => !secret.isPRMRSecret);
|
const applicationSecrets = secrets.filter((secret) => !secret.isPRMRSecret);
|
||||||
const PRMRSecrets = secrets.filter((secret) => secret.isPRMRSecret);
|
const PRMRSecrets = secrets.filter((secret) => secret.isPRMRSecret);
|
||||||
const application = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } });
|
|
||||||
const { stdout } = await executeDockerCmd({ dockerId: application.destinationDocker.id, command: `docker container ls --filter 'name=${id}-' --format "{{json .}}"` })
|
|
||||||
if (stdout === '') {
|
|
||||||
return {
|
|
||||||
containers: [],
|
|
||||||
applicationSecrets: [],
|
|
||||||
PRMRSecrets: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const containers = formatLabelsOnDocker(stdout).filter(container => container.Labels['coolify.configuration'] && container.Labels['coolify.type'] === 'standalone-application')
|
|
||||||
|
|
||||||
const jsonContainers = containers
|
|
||||||
.map((container) =>
|
|
||||||
JSON.parse(Buffer.from(container.Labels['coolify.configuration'], 'base64').toString())
|
|
||||||
)
|
|
||||||
.filter((container) => {
|
|
||||||
return container.pullmergeRequestId && container.applicationId === id;
|
|
||||||
});
|
|
||||||
return {
|
return {
|
||||||
containers: jsonContainers,
|
|
||||||
applicationSecrets: applicationSecrets.sort((a, b) => {
|
applicationSecrets: applicationSecrets.sort((a, b) => {
|
||||||
return ('' + a.name).localeCompare(b.name);
|
return ('' + a.name).localeCompare(b.name);
|
||||||
}),
|
}),
|
||||||
@@ -970,7 +1156,7 @@ export async function getApplicationLogs(request: FastifyRequest<GetApplicationL
|
|||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export async function getBuildLogs(request: FastifyRequest<GetBuildLogs>) {
|
export async function getBuilds(request: FastifyRequest<GetBuilds>) {
|
||||||
try {
|
try {
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
let { buildId, skip = 0 } = request.query
|
let { buildId, skip = 0 } = request.query
|
||||||
@@ -987,17 +1173,15 @@ export async function getBuildLogs(request: FastifyRequest<GetBuildLogs>) {
|
|||||||
builds = await prisma.build.findMany({
|
builds = await prisma.build.findMany({
|
||||||
where: { applicationId: id },
|
where: { applicationId: id },
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
take: 5,
|
take: 5 + skip
|
||||||
skip
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
builds = builds.map((build) => {
|
builds = builds.map((build) => {
|
||||||
const updatedAt = day(build.updatedAt).utc();
|
if (build.status === 'running') {
|
||||||
build.took = updatedAt.diff(day(build.createdAt)) / 1000;
|
build.elapsed = (day().utc().diff(day(build.createdAt)) / 1000).toFixed(0);
|
||||||
build.since = updatedAt.fromNow();
|
}
|
||||||
return build;
|
return build
|
||||||
});
|
})
|
||||||
return {
|
return {
|
||||||
builds,
|
builds,
|
||||||
buildCount
|
buildCount
|
||||||
@@ -1009,22 +1193,49 @@ export async function getBuildLogs(request: FastifyRequest<GetBuildLogs>) {
|
|||||||
|
|
||||||
export async function getBuildIdLogs(request: FastifyRequest<GetBuildIdLogs>) {
|
export async function getBuildIdLogs(request: FastifyRequest<GetBuildIdLogs>) {
|
||||||
try {
|
try {
|
||||||
const { buildId } = request.params
|
// TODO: Fluentbit could still hold the logs, so we need to check if the logs are done
|
||||||
|
const { buildId, id } = request.params
|
||||||
let { sequence = 0 } = request.query
|
let { sequence = 0 } = request.query
|
||||||
if (typeof sequence !== 'number') {
|
if (typeof sequence !== 'number') {
|
||||||
sequence = Number(sequence)
|
sequence = Number(sequence)
|
||||||
}
|
}
|
||||||
let logs = await prisma.buildLog.findMany({
|
let file = `/app/logs/${id}_buildlog_${buildId}.csv`
|
||||||
where: { buildId, time: { gt: sequence } },
|
if (isDev) {
|
||||||
orderBy: { time: 'asc' }
|
file = `${process.cwd()}/../../logs/${id}_buildlog_${buildId}.csv`
|
||||||
});
|
}
|
||||||
const data = await prisma.build.findFirst({ where: { id: buildId } });
|
const data = await prisma.build.findFirst({ where: { id: buildId } });
|
||||||
const createdAt = day(data.createdAt).utc();
|
const createdAt = day(data.createdAt).utc();
|
||||||
|
try {
|
||||||
|
await fs.stat(file)
|
||||||
|
} catch (error) {
|
||||||
|
let logs = await prisma.buildLog.findMany({
|
||||||
|
where: { buildId, time: { gt: sequence } },
|
||||||
|
orderBy: { time: 'asc' }
|
||||||
|
});
|
||||||
|
const data = await prisma.build.findFirst({ where: { id: buildId } });
|
||||||
|
const createdAt = day(data.createdAt).utc();
|
||||||
|
return {
|
||||||
|
logs: logs.map(log => {
|
||||||
|
log.time = Number(log.time)
|
||||||
|
return log
|
||||||
|
}),
|
||||||
|
fromDb: true,
|
||||||
|
took: day().diff(createdAt) / 1000,
|
||||||
|
status: data?.status || 'queued'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let fileLogs = (await fs.readFile(file)).toString()
|
||||||
|
let decryptedLogs = await csv({ noheader: true }).fromString(fileLogs)
|
||||||
|
let logs = decryptedLogs.map(log => {
|
||||||
|
const parsed = {
|
||||||
|
time: log['field1'],
|
||||||
|
line: decrypt(log['field2'] + '","' + log['field3'])
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
|
}).filter(log => log.time > sequence)
|
||||||
return {
|
return {
|
||||||
logs: logs.map(log => {
|
logs,
|
||||||
log.time = Number(log.time)
|
fromDb: false,
|
||||||
return log
|
|
||||||
}),
|
|
||||||
took: day().diff(createdAt) / 1000,
|
took: day().diff(createdAt) / 1000,
|
||||||
status: data?.status || 'queued'
|
status: data?.status || 'queued'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { FastifyPluginAsync } from 'fastify';
|
import { FastifyPluginAsync } from 'fastify';
|
||||||
import { OnlyId } from '../../../../types';
|
import { OnlyId } from '../../../../types';
|
||||||
import { cancelDeployment, checkDNS, checkDomain, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildLogs, getBuildPack, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getSecrets, getStorages, getUsage, listApplications, newApplication, restartApplication, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveConnectedDatabase, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication } from './handlers';
|
import { cancelDeployment, checkDNS, checkDomain, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildPack, getBuilds, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getPreviewStatus, getSecrets, getStorages, getUsage, listApplications, loadPreviews, newApplication, restartApplication, restartPreview, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveConnectedDatabase, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication, updatePreviewSecret, updateSecret } from './handlers';
|
||||||
|
|
||||||
import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, GetImages, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types';
|
import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuilds, GetImages, RestartPreviewApplication, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types';
|
||||||
|
|
||||||
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||||
fastify.addHook('onRequest', async (request) => {
|
fastify.addHook('onRequest', async (request) => {
|
||||||
@@ -30,6 +30,8 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
|||||||
|
|
||||||
fastify.get<OnlyId>('/:id/secrets', async (request) => await getSecrets(request));
|
fastify.get<OnlyId>('/:id/secrets', async (request) => await getSecrets(request));
|
||||||
fastify.post<SaveSecret>('/:id/secrets', async (request, reply) => await saveSecret(request, reply));
|
fastify.post<SaveSecret>('/:id/secrets', async (request, reply) => await saveSecret(request, reply));
|
||||||
|
fastify.put<SaveSecret>('/:id/secrets', async (request, reply) => await updateSecret(request, reply));
|
||||||
|
fastify.put<SaveSecret>('/:id/secrets/preview', async (request, reply) => await updatePreviewSecret(request, reply));
|
||||||
fastify.delete<DeleteSecret>('/:id/secrets', async (request) => await deleteSecret(request));
|
fastify.delete<DeleteSecret>('/:id/secrets', async (request) => await deleteSecret(request));
|
||||||
|
|
||||||
fastify.get<OnlyId>('/:id/storages', async (request) => await getStorages(request));
|
fastify.get<OnlyId>('/:id/storages', async (request) => await getStorages(request));
|
||||||
@@ -37,9 +39,12 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
|||||||
fastify.delete<DeleteStorage>('/:id/storages', async (request) => await deleteStorage(request));
|
fastify.delete<DeleteStorage>('/:id/storages', async (request) => await deleteStorage(request));
|
||||||
|
|
||||||
fastify.get<OnlyId>('/:id/previews', async (request) => await getPreviews(request));
|
fastify.get<OnlyId>('/:id/previews', async (request) => await getPreviews(request));
|
||||||
|
fastify.post<OnlyId>('/:id/previews/load', async (request) => await loadPreviews(request));
|
||||||
|
fastify.get<RestartPreviewApplication>('/:id/previews/:pullmergeRequestId/status', async (request) => await getPreviewStatus(request));
|
||||||
|
fastify.post<RestartPreviewApplication>('/:id/previews/:pullmergeRequestId/restart', async (request, reply) => await restartPreview(request, reply));
|
||||||
|
|
||||||
fastify.get<GetApplicationLogs>('/:id/logs', async (request) => await getApplicationLogs(request));
|
fastify.get<GetApplicationLogs>('/:id/logs', async (request) => await getApplicationLogs(request));
|
||||||
fastify.get<GetBuildLogs>('/:id/logs/build', async (request) => await getBuildLogs(request));
|
fastify.get<GetBuilds>('/:id/logs/build', async (request) => await getBuilds(request));
|
||||||
fastify.get<GetBuildIdLogs>('/:id/logs/build/:buildId', async (request) => await getBuildIdLogs(request));
|
fastify.get<GetBuildIdLogs>('/:id/logs/build/:buildId', async (request) => await getBuildIdLogs(request));
|
||||||
|
|
||||||
fastify.get('/:id/usage', async (request) => await getUsage(request))
|
fastify.get('/:id/usage', async (request) => await getUsage(request))
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export interface SaveApplication extends OnlyId {
|
|||||||
}
|
}
|
||||||
export interface SaveApplicationSettings extends OnlyId {
|
export interface SaveApplicationSettings extends OnlyId {
|
||||||
Querystring: { domain: string; };
|
Querystring: { domain: string; };
|
||||||
Body: { debug: boolean; previews: boolean; dualCerts: boolean; autodeploy: boolean; branch: string; projectId: number; isBot: boolean; isDBBranching: boolean };
|
Body: { debug: boolean; previews: boolean; dualCerts: boolean; autodeploy: boolean; branch: string; projectId: number; isBot: boolean; isDBBranching: boolean, isCustomSSL: boolean };
|
||||||
}
|
}
|
||||||
export interface DeleteApplication extends OnlyId {
|
export interface DeleteApplication extends OnlyId {
|
||||||
Querystring: { domain: string; };
|
Querystring: { domain: string; };
|
||||||
@@ -65,7 +65,7 @@ export interface SaveSecret extends OnlyId {
|
|||||||
name: string,
|
name: string,
|
||||||
value: string,
|
value: string,
|
||||||
isBuildSecret: boolean,
|
isBuildSecret: boolean,
|
||||||
isPRMRSecret: boolean,
|
previewSecret: boolean,
|
||||||
isNew: boolean
|
isNew: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,7 +89,7 @@ export interface GetApplicationLogs extends OnlyId {
|
|||||||
since: number,
|
since: number,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export interface GetBuildLogs extends OnlyId {
|
export interface GetBuilds extends OnlyId {
|
||||||
Querystring: {
|
Querystring: {
|
||||||
buildId: string
|
buildId: string
|
||||||
skip: number,
|
skip: number,
|
||||||
@@ -97,6 +97,7 @@ export interface GetBuildLogs extends OnlyId {
|
|||||||
}
|
}
|
||||||
export interface GetBuildIdLogs {
|
export interface GetBuildIdLogs {
|
||||||
Params: {
|
Params: {
|
||||||
|
id: string,
|
||||||
buildId: string
|
buildId: string
|
||||||
},
|
},
|
||||||
Querystring: {
|
Querystring: {
|
||||||
@@ -126,4 +127,10 @@ export interface StopPreviewApplication extends OnlyId {
|
|||||||
Body: {
|
Body: {
|
||||||
pullmergeRequestId: string | null,
|
pullmergeRequestId: string | null,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
export interface RestartPreviewApplication {
|
||||||
|
Params: {
|
||||||
|
id: string,
|
||||||
|
pullmergeRequestId: string | null,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,7 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
|||||||
version,
|
version,
|
||||||
whiteLabeled: process.env.COOLIFY_WHITE_LABELED === 'true',
|
whiteLabeled: process.env.COOLIFY_WHITE_LABELED === 'true',
|
||||||
whiteLabeledIcon: process.env.COOLIFY_WHITE_LABELED_ICON,
|
whiteLabeledIcon: process.env.COOLIFY_WHITE_LABELED_ICON,
|
||||||
|
isRegistrationEnabled: settings.isRegistrationEnabled,
|
||||||
}
|
}
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import type { FastifyRequest } from 'fastify';
|
|||||||
import { FastifyReply } from 'fastify';
|
import { FastifyReply } from 'fastify';
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import { ComposeFile, createDirectories, decrypt, encrypt, errorHandler, executeDockerCmd, generateDatabaseConfiguration, generatePassword, getContainerUsage, getDatabaseImage, getDatabaseVersions, getFreePublicPort, listSettings, makeLabelForStandaloneDatabase, prisma, startTraefikTCPProxy, stopDatabaseContainer, stopTcpHttpProxy, supportedDatabaseTypesAndVersions, uniqueName, updatePasswordInDb } from '../../../../lib/common';
|
import { ComposeFile, createDirectories, decrypt, defaultComposeConfiguration, encrypt, errorHandler, executeDockerCmd, generateDatabaseConfiguration, generatePassword, getContainerUsage, getDatabaseImage, getDatabaseVersions, getFreePublicPort, listSettings, makeLabelForStandaloneDatabase, prisma, startTraefikTCPProxy, stopDatabaseContainer, stopTcpHttpProxy, supportedDatabaseTypesAndVersions, uniqueName, updatePasswordInDb } from '../../../../lib/common';
|
||||||
import { day } from '../../../../lib/dayjs';
|
import { day } from '../../../../lib/dayjs';
|
||||||
|
|
||||||
import { GetDatabaseLogs, OnlyId, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSettings, SaveVersion } from '../../../../types';
|
import type { OnlyId } from '../../../../types';
|
||||||
import { DeleteDatabase, SaveDatabaseType } from './types';
|
import type { DeleteDatabase, DeleteDatabaseSecret, GetDatabaseLogs, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSecret, SaveDatabaseSettings, SaveDatabaseType, SaveVersion } from './types';
|
||||||
|
|
||||||
export async function listDatabases(request: FastifyRequest) {
|
export async function listDatabases(request: FastifyRequest) {
|
||||||
try {
|
try {
|
||||||
@@ -61,16 +61,18 @@ export async function getDatabaseStatus(request: FastifyRequest<OnlyId>) {
|
|||||||
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
||||||
include: { destinationDocker: true, settings: true }
|
include: { destinationDocker: true, settings: true }
|
||||||
});
|
});
|
||||||
const { destinationDockerId, destinationDocker } = database;
|
if (database) {
|
||||||
if (destinationDockerId) {
|
const { destinationDockerId, destinationDocker } = database;
|
||||||
try {
|
if (destinationDockerId) {
|
||||||
const { stdout } = await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker inspect --format '{{json .State}}' ${id}` })
|
try {
|
||||||
|
const { stdout } = await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker inspect --format '{{json .State}}' ${id}` })
|
||||||
|
|
||||||
if (JSON.parse(stdout).Running) {
|
if (JSON.parse(stdout).Running) {
|
||||||
isRunning = true;
|
isRunning = true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
//
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
//
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -92,15 +94,14 @@ export async function getDatabase(request: FastifyRequest<OnlyId>) {
|
|||||||
if (!database) {
|
if (!database) {
|
||||||
throw { status: 404, message: 'Database not found.' }
|
throw { status: 404, message: 'Database not found.' }
|
||||||
}
|
}
|
||||||
const { arch } = await listSettings();
|
const settings = await listSettings();
|
||||||
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
|
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
|
||||||
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
|
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
|
||||||
const configuration = generateDatabaseConfiguration(database, arch);
|
const configuration = generateDatabaseConfiguration(database, settings.arch);
|
||||||
const settings = await listSettings();
|
|
||||||
return {
|
return {
|
||||||
privatePort: configuration?.privatePort,
|
privatePort: configuration?.privatePort,
|
||||||
database,
|
database,
|
||||||
versions: await getDatabaseVersions(database.type, arch),
|
versions: await getDatabaseVersions(database.type, settings.arch),
|
||||||
settings
|
settings
|
||||||
};
|
};
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
@@ -220,7 +221,7 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
|
|||||||
|
|
||||||
const database = await prisma.database.findFirst({
|
const database = await prisma.database.findFirst({
|
||||||
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
||||||
include: { destinationDocker: true, settings: true }
|
include: { destinationDocker: true, settings: true, databaseSecret: true }
|
||||||
});
|
});
|
||||||
const { arch } = await listSettings();
|
const { arch } = await listSettings();
|
||||||
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
|
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
|
||||||
@@ -230,7 +231,8 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
|
|||||||
destinationDockerId,
|
destinationDockerId,
|
||||||
destinationDocker,
|
destinationDocker,
|
||||||
publicPort,
|
publicPort,
|
||||||
settings: { isPublic }
|
settings: { isPublic },
|
||||||
|
databaseSecret
|
||||||
} = database;
|
} = database;
|
||||||
const { privatePort, command, environmentVariables, image, volume, ulimits } =
|
const { privatePort, command, environmentVariables, image, volume, ulimits } =
|
||||||
generateDatabaseConfiguration(database, arch);
|
generateDatabaseConfiguration(database, arch);
|
||||||
@@ -240,7 +242,11 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
|
|||||||
const labels = await makeLabelForStandaloneDatabase({ id, image, volume });
|
const labels = await makeLabelForStandaloneDatabase({ id, image, volume });
|
||||||
|
|
||||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||||
|
if (databaseSecret.length > 0) {
|
||||||
|
databaseSecret.forEach((secret) => {
|
||||||
|
environmentVariables[secret.name] = decrypt(secret.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
const composeFile: ComposeFile = {
|
const composeFile: ComposeFile = {
|
||||||
version: '3.8',
|
version: '3.8',
|
||||||
services: {
|
services: {
|
||||||
@@ -248,20 +254,11 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
|
|||||||
container_name: id,
|
container_name: id,
|
||||||
image,
|
image,
|
||||||
command,
|
command,
|
||||||
networks: [network],
|
|
||||||
environment: environmentVariables,
|
environment: environmentVariables,
|
||||||
volumes: [volume],
|
volumes: [volume],
|
||||||
ulimits,
|
ulimits,
|
||||||
labels,
|
labels,
|
||||||
restart: 'always',
|
...defaultComposeConfiguration(network),
|
||||||
deploy: {
|
|
||||||
restart_policy: {
|
|
||||||
condition: 'on-failure',
|
|
||||||
delay: '5s',
|
|
||||||
max_attempts: 3,
|
|
||||||
window: '120s'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
networks: {
|
networks: {
|
||||||
@@ -271,25 +268,16 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
|
|||||||
},
|
},
|
||||||
volumes: {
|
volumes: {
|
||||||
[volumeName]: {
|
[volumeName]: {
|
||||||
external: true
|
name: volumeName,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||||
try {
|
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up -d` })
|
||||||
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker volume create ${volumeName}` })
|
if (isPublic) await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
|
||||||
} catch (error) { }
|
return {};
|
||||||
try {
|
|
||||||
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up -d` })
|
|
||||||
if (isPublic) await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
|
|
||||||
return {};
|
|
||||||
} catch (error) {
|
|
||||||
throw {
|
|
||||||
error
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
}
|
}
|
||||||
@@ -376,6 +364,7 @@ export async function deleteDatabase(request: FastifyRequest<DeleteDatabase>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
await prisma.databaseSettings.deleteMany({ where: { databaseId: id } });
|
await prisma.databaseSettings.deleteMany({ where: { databaseId: id } });
|
||||||
|
await prisma.databaseSecret.deleteMany({ where: { databaseId: id } });
|
||||||
await prisma.database.delete({ where: { id } });
|
await prisma.database.delete({ where: { id } });
|
||||||
return {}
|
return {}
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
@@ -436,10 +425,10 @@ export async function saveDatabaseSettings(request: FastifyRequest<SaveDatabaseS
|
|||||||
|
|
||||||
let publicPort = null
|
let publicPort = null
|
||||||
|
|
||||||
const { destinationDocker: { id: dockerId } } = await prisma.database.findUnique({ where: { id }, include: { destinationDocker: true } })
|
const { destinationDocker: { remoteEngine, engine, remoteIpAddress } } = await prisma.database.findUnique({ where: { id }, include: { destinationDocker: true } })
|
||||||
|
|
||||||
if (isPublic) {
|
if (isPublic) {
|
||||||
publicPort = await getFreePublicPort(id, dockerId);
|
publicPort = await getFreePublicPort({ id, remoteEngine, engine, remoteIpAddress });
|
||||||
}
|
}
|
||||||
await prisma.database.update({
|
await prisma.database.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
@@ -471,4 +460,69 @@ export async function saveDatabaseSettings(request: FastifyRequest<SaveDatabaseS
|
|||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export async function getDatabaseSecrets(request: FastifyRequest<OnlyId>) {
|
||||||
|
try {
|
||||||
|
const { id } = request.params
|
||||||
|
let secrets = await prisma.databaseSecret.findMany({
|
||||||
|
where: { databaseId: id },
|
||||||
|
orderBy: { createdAt: 'desc' }
|
||||||
|
});
|
||||||
|
secrets = secrets.map((secret) => {
|
||||||
|
secret.value = decrypt(secret.value);
|
||||||
|
return secret;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
secrets
|
||||||
|
}
|
||||||
|
} catch ({ status, message }) {
|
||||||
|
return errorHandler({ status, message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveDatabaseSecret(request: FastifyRequest<SaveDatabaseSecret>, reply: FastifyReply) {
|
||||||
|
try {
|
||||||
|
const { id } = request.params
|
||||||
|
let { name, value, isNew } = request.body
|
||||||
|
|
||||||
|
if (isNew) {
|
||||||
|
const found = await prisma.databaseSecret.findFirst({ where: { name, databaseId: id } });
|
||||||
|
if (found) {
|
||||||
|
throw `Secret ${name} already exists.`
|
||||||
|
} else {
|
||||||
|
value = encrypt(value.trim());
|
||||||
|
await prisma.databaseSecret.create({
|
||||||
|
data: { name, value, database: { connect: { id } } }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
value = encrypt(value.trim());
|
||||||
|
const found = await prisma.databaseSecret.findFirst({ where: { databaseId: id, name } });
|
||||||
|
|
||||||
|
if (found) {
|
||||||
|
await prisma.databaseSecret.updateMany({
|
||||||
|
where: { databaseId: id, name },
|
||||||
|
data: { value }
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await prisma.databaseSecret.create({
|
||||||
|
data: { name, value, database: { connect: { id } } }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reply.code(201).send()
|
||||||
|
} catch ({ status, message }) {
|
||||||
|
return errorHandler({ status, message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function deleteDatabaseSecret(request: FastifyRequest<DeleteDatabaseSecret>) {
|
||||||
|
try {
|
||||||
|
const { id } = request.params
|
||||||
|
const { name } = request.body
|
||||||
|
await prisma.databaseSecret.deleteMany({ where: { databaseId: id, name } });
|
||||||
|
return {}
|
||||||
|
} catch ({ status, message }) {
|
||||||
|
return errorHandler({ status, message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { FastifyPluginAsync } from 'fastify';
|
import { FastifyPluginAsync } from 'fastify';
|
||||||
import { deleteDatabase, getDatabase, getDatabaseLogs, getDatabaseStatus, getDatabaseTypes, getDatabaseUsage, getVersions, listDatabases, newDatabase, saveDatabase, saveDatabaseDestination, saveDatabaseSettings, saveDatabaseType, saveVersion, startDatabase, stopDatabase } from './handlers';
|
import { deleteDatabase, deleteDatabaseSecret, getDatabase, getDatabaseLogs, getDatabaseSecrets, getDatabaseStatus, getDatabaseTypes, getDatabaseUsage, getVersions, listDatabases, newDatabase, saveDatabase, saveDatabaseDestination, saveDatabaseSecret, saveDatabaseSettings, saveDatabaseType, saveVersion, startDatabase, stopDatabase } from './handlers';
|
||||||
|
|
||||||
import type { DeleteDatabase, GetDatabaseLogs, OnlyId, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSettings, SaveVersion } from '../../../../types';
|
import type { OnlyId } from '../../../../types';
|
||||||
import type { SaveDatabaseType } from './types';
|
|
||||||
|
import type { DeleteDatabase, SaveDatabaseType, DeleteDatabaseSecret, GetDatabaseLogs, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSecret, SaveDatabaseSettings, SaveVersion } from './types';
|
||||||
|
|
||||||
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||||
fastify.addHook('onRequest', async (request) => {
|
fastify.addHook('onRequest', async (request) => {
|
||||||
@@ -19,6 +20,10 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
|||||||
|
|
||||||
fastify.post<SaveDatabaseSettings>('/:id/settings', async (request) => await saveDatabaseSettings(request));
|
fastify.post<SaveDatabaseSettings>('/:id/settings', async (request) => await saveDatabaseSettings(request));
|
||||||
|
|
||||||
|
fastify.get<OnlyId>('/:id/secrets', async (request) => await getDatabaseSecrets(request));
|
||||||
|
fastify.post<SaveDatabaseSecret>('/:id/secrets', async (request, reply) => await saveDatabaseSecret(request, reply));
|
||||||
|
fastify.delete<DeleteDatabaseSecret>('/:id/secrets', async (request) => await deleteDatabaseSecret(request));
|
||||||
|
|
||||||
fastify.get('/:id/configuration/type', async (request) => await getDatabaseTypes(request));
|
fastify.get('/:id/configuration/type', async (request) => await getDatabaseTypes(request));
|
||||||
fastify.post<SaveDatabaseType>('/:id/configuration/type', async (request, reply) => await saveDatabaseType(request, reply));
|
fastify.post<SaveDatabaseType>('/:id/configuration/type', async (request, reply) => await saveDatabaseType(request, reply));
|
||||||
|
|
||||||
|
|||||||
@@ -5,4 +5,51 @@ export interface SaveDatabaseType extends OnlyId {
|
|||||||
}
|
}
|
||||||
export interface DeleteDatabase extends OnlyId {
|
export interface DeleteDatabase extends OnlyId {
|
||||||
Body: { force: string }
|
Body: { force: string }
|
||||||
}
|
}
|
||||||
|
export interface SaveVersion extends OnlyId {
|
||||||
|
Body: {
|
||||||
|
version: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export interface SaveDatabaseDestination extends OnlyId {
|
||||||
|
Body: {
|
||||||
|
destinationId: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export interface GetDatabaseLogs extends OnlyId {
|
||||||
|
Querystring: {
|
||||||
|
since: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export interface SaveDatabase extends OnlyId {
|
||||||
|
Body: {
|
||||||
|
name: string,
|
||||||
|
defaultDatabase: string,
|
||||||
|
dbUser: string,
|
||||||
|
dbUserPassword: string,
|
||||||
|
rootUser: string,
|
||||||
|
rootUserPassword: string,
|
||||||
|
version: string,
|
||||||
|
isRunning: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export interface SaveDatabaseSettings extends OnlyId {
|
||||||
|
Body: {
|
||||||
|
isPublic: boolean,
|
||||||
|
appendOnly: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SaveDatabaseSecret extends OnlyId {
|
||||||
|
Body: {
|
||||||
|
name: string,
|
||||||
|
value: string,
|
||||||
|
isNew: string,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export interface DeleteDatabaseSecret extends OnlyId {
|
||||||
|
Body: {
|
||||||
|
name: string,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ export async function getDestinationStatus(request: FastifyRequest<OnlyId>) {
|
|||||||
try {
|
try {
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
const destination = await prisma.destinationDocker.findUnique({ where: { id } })
|
const destination = await prisma.destinationDocker.findUnique({ where: { id } })
|
||||||
const isRunning = await checkContainer({ dockerId: destination.id, container: 'coolify-proxy', remove: true })
|
const { found: isRunning } = await checkContainer({ dockerId: destination.id, container: 'coolify-proxy', remove: true })
|
||||||
return {
|
return {
|
||||||
isRunning
|
isRunning
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,64 @@
|
|||||||
import os from 'node:os';
|
import axios from "axios";
|
||||||
import osu from 'node-os-utils';
|
import { compareVersions } from "compare-versions";
|
||||||
import axios from 'axios';
|
import cuid from "cuid";
|
||||||
import { compareVersions } from 'compare-versions';
|
import bcrypt from "bcryptjs";
|
||||||
import cuid from 'cuid';
|
import {
|
||||||
import bcrypt from 'bcryptjs';
|
asyncExecShell,
|
||||||
import { asyncExecShell, asyncSleep, cleanupDockerStorage, errorHandler, isDev, listSettings, prisma, uniqueName, version } from '../../../lib/common';
|
asyncSleep,
|
||||||
import { supportedServiceTypesAndVersions } from '../../../lib/services/supportedVersions';
|
cleanupDockerStorage,
|
||||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
errorHandler,
|
||||||
import type { Login, Update } from '.';
|
isDev,
|
||||||
import type { GetCurrentUser } from './types';
|
listSettings,
|
||||||
|
prisma,
|
||||||
|
uniqueName,
|
||||||
|
version,
|
||||||
|
} from "../../../lib/common";
|
||||||
|
import { supportedServiceTypesAndVersions } from "../../../lib/services/supportedVersions";
|
||||||
|
import { scheduler } from "../../../lib/scheduler";
|
||||||
|
import type { FastifyReply, FastifyRequest } from "fastify";
|
||||||
|
import type { Login, Update } from ".";
|
||||||
|
import type { GetCurrentUser } from "./types";
|
||||||
|
|
||||||
export async function hashPassword(password: string): Promise<string> {
|
export async function hashPassword(password: string): Promise<string> {
|
||||||
const saltRounds = 15;
|
const saltRounds = 15;
|
||||||
return bcrypt.hash(password, saltRounds);
|
return bcrypt.hash(password, saltRounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cleanupManually() {
|
export async function cleanupManually(request: FastifyRequest) {
|
||||||
try {
|
try {
|
||||||
const destination = await prisma.destinationDocker.findFirst({ where: { engine: '/var/run/docker.sock' } })
|
const { serverId } = request.body;
|
||||||
await cleanupDockerStorage(destination.id, true, true)
|
const destination = await prisma.destinationDocker.findUnique({
|
||||||
return {}
|
where: { id: serverId },
|
||||||
|
});
|
||||||
|
await cleanupDockerStorage(destination.id, true, true);
|
||||||
|
return {};
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export async function checkUpdate(request: FastifyRequest) {
|
export async function checkUpdate(request: FastifyRequest) {
|
||||||
try {
|
try {
|
||||||
const isStaging = request.hostname === 'staging.coolify.io'
|
const isStaging =
|
||||||
|
request.hostname === "staging.coolify.io" ||
|
||||||
|
request.hostname === "arm.coolify.io";
|
||||||
const currentVersion = version;
|
const currentVersion = version;
|
||||||
const { data: versions } = await axios.get(
|
const { data: versions } = await axios.get(
|
||||||
`https://get.coollabs.io/versions.json?appId=${process.env['COOLIFY_APP_ID']}&version=${currentVersion}`
|
`https://get.coollabs.io/versions.json?appId=${process.env["COOLIFY_APP_ID"]}&version=${currentVersion}`
|
||||||
);
|
);
|
||||||
const latestVersion = versions['coolify'].main.version
|
const latestVersion = versions["coolify"].main.version;
|
||||||
const isUpdateAvailable = compareVersions(latestVersion, currentVersion);
|
const isUpdateAvailable = compareVersions(latestVersion, currentVersion);
|
||||||
if (isStaging) {
|
if (isStaging) {
|
||||||
return {
|
return {
|
||||||
isUpdateAvailable: true,
|
isUpdateAvailable: true,
|
||||||
latestVersion: 'next'
|
latestVersion: "next",
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
isUpdateAvailable: isStaging ? true : isUpdateAvailable === 1,
|
isUpdateAvailable: isStaging ? true : isUpdateAvailable === 1,
|
||||||
latestVersion
|
latestVersion,
|
||||||
};
|
};
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,16 +66,14 @@ export async function update(request: FastifyRequest<Update>) {
|
|||||||
const { latestVersion } = request.body;
|
const { latestVersion } = request.body;
|
||||||
try {
|
try {
|
||||||
if (!isDev) {
|
if (!isDev) {
|
||||||
const { isAutoUpdateEnabled } = (await prisma.setting.findFirst()) || {
|
const { isAutoUpdateEnabled } = await prisma.setting.findFirst();
|
||||||
isAutoUpdateEnabled: false
|
|
||||||
};
|
|
||||||
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
|
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
|
||||||
await asyncExecShell(`env | grep COOLIFY > .env`);
|
await asyncExecShell(`env | grep COOLIFY > .env`);
|
||||||
await asyncExecShell(
|
await asyncExecShell(
|
||||||
`sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env`
|
`sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env`
|
||||||
);
|
);
|
||||||
await asyncExecShell(
|
await asyncExecShell(
|
||||||
`docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify && docker rm coolify && docker compose up -d --force-recreate"`
|
`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-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"`
|
||||||
);
|
);
|
||||||
return {};
|
return {};
|
||||||
} else {
|
} else {
|
||||||
@@ -69,13 +81,27 @@ export async function update(request: FastifyRequest<Update>) {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function resetQueue(request: FastifyRequest<any>) {
|
||||||
|
try {
|
||||||
|
const teamId = request.user.teamId;
|
||||||
|
if (teamId === "0") {
|
||||||
|
await prisma.build.updateMany({
|
||||||
|
where: { status: { in: ["queued", "running"] } },
|
||||||
|
data: { status: "canceled" },
|
||||||
|
});
|
||||||
|
scheduler.workers.get("deployApplication").postMessage("cancel");
|
||||||
|
}
|
||||||
|
} catch ({ status, message }) {
|
||||||
|
return errorHandler({ status, message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export async function restartCoolify(request: FastifyRequest<any>) {
|
export async function restartCoolify(request: FastifyRequest<any>) {
|
||||||
try {
|
try {
|
||||||
const teamId = request.user.teamId;
|
const teamId = request.user.teamId;
|
||||||
if (teamId === '0') {
|
if (teamId === "0") {
|
||||||
if (!isDev) {
|
if (!isDev) {
|
||||||
asyncExecShell(`docker restart coolify`);
|
asyncExecShell(`docker restart coolify`);
|
||||||
return {};
|
return {};
|
||||||
@@ -83,135 +109,141 @@ export async function restartCoolify(request: FastifyRequest<any>) {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw { status: 500, message: 'You are not authorized to restart Coolify.' };
|
throw {
|
||||||
} catch ({ status, message }) {
|
status: 500,
|
||||||
return errorHandler({ status, message })
|
message: "You are not authorized to restart Coolify.",
|
||||||
}
|
|
||||||
}
|
|
||||||
export async function showUsage() {
|
|
||||||
try {
|
|
||||||
return {
|
|
||||||
usage: {
|
|
||||||
uptime: os.uptime(),
|
|
||||||
memory: await osu.mem.info(),
|
|
||||||
cpu: {
|
|
||||||
load: os.loadavg(),
|
|
||||||
usage: await osu.cpu.usage(),
|
|
||||||
count: os.cpus().length
|
|
||||||
},
|
|
||||||
disk: await osu.drive.info('/')
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
};
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function showDashboard(request: FastifyRequest) {
|
export async function showDashboard(request: FastifyRequest) {
|
||||||
try {
|
try {
|
||||||
const userId = request.user.userId;
|
const userId = request.user.userId;
|
||||||
const teamId = request.user.teamId;
|
const teamId = request.user.teamId;
|
||||||
const applications = await prisma.application.findMany({
|
const applications = await prisma.application.findMany({
|
||||||
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } },
|
||||||
include: { settings: true }
|
include: { settings: true, destinationDocker: true, teams: true },
|
||||||
});
|
});
|
||||||
const databases = await prisma.database.findMany({
|
const databases = await prisma.database.findMany({
|
||||||
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } },
|
||||||
include: { settings: true }
|
include: { settings: true, destinationDocker: true, teams: true },
|
||||||
});
|
});
|
||||||
const services = await prisma.service.findMany({
|
const services = await prisma.service.findMany({
|
||||||
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }
|
where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } },
|
||||||
|
include: { destinationDocker: true, teams: true },
|
||||||
|
});
|
||||||
|
const gitSources = await prisma.gitSource.findMany({
|
||||||
|
where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } },
|
||||||
|
include: { teams: true },
|
||||||
|
});
|
||||||
|
const destinations = await prisma.destinationDocker.findMany({
|
||||||
|
where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } },
|
||||||
|
include: { teams: true },
|
||||||
});
|
});
|
||||||
const settings = await listSettings();
|
const settings = await listSettings();
|
||||||
return {
|
return {
|
||||||
applications,
|
applications,
|
||||||
databases,
|
databases,
|
||||||
services,
|
services,
|
||||||
|
gitSources,
|
||||||
|
destinations,
|
||||||
settings,
|
settings,
|
||||||
};
|
};
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function login(request: FastifyRequest<Login>, reply: FastifyReply) {
|
export async function login(
|
||||||
|
request: FastifyRequest<Login>,
|
||||||
|
reply: FastifyReply
|
||||||
|
) {
|
||||||
if (request.user) {
|
if (request.user) {
|
||||||
return reply.redirect('/dashboard');
|
return reply.redirect("/dashboard");
|
||||||
} else {
|
} else {
|
||||||
const { email, password, isLogin } = request.body || {};
|
const { email, password, isLogin } = request.body || {};
|
||||||
if (!email || !password) {
|
if (!email || !password) {
|
||||||
throw { status: 500, message: 'Email and password are required.' };
|
throw { status: 500, message: "Email and password are required." };
|
||||||
}
|
}
|
||||||
const users = await prisma.user.count();
|
const users = await prisma.user.count();
|
||||||
const userFound = await prisma.user.findUnique({
|
const userFound = await prisma.user.findUnique({
|
||||||
where: { email },
|
where: { email },
|
||||||
include: { teams: true, permission: true },
|
include: { teams: true, permission: true },
|
||||||
rejectOnNotFound: false
|
rejectOnNotFound: false,
|
||||||
});
|
});
|
||||||
if (!userFound && isLogin) {
|
if (!userFound && isLogin) {
|
||||||
throw { status: 500, message: 'User not found.' };
|
throw { status: 500, message: "User not found." };
|
||||||
}
|
}
|
||||||
const { isRegistrationEnabled, id } = await prisma.setting.findFirst()
|
const { isRegistrationEnabled, id } = await prisma.setting.findFirst();
|
||||||
let uid = cuid();
|
let uid = cuid();
|
||||||
let permission = 'read';
|
let permission = "read";
|
||||||
let isAdmin = false;
|
let isAdmin = false;
|
||||||
|
|
||||||
if (users === 0) {
|
if (users === 0) {
|
||||||
await prisma.setting.update({ where: { id }, data: { isRegistrationEnabled: false } });
|
await prisma.setting.update({
|
||||||
uid = '0';
|
where: { id },
|
||||||
|
data: { isRegistrationEnabled: false },
|
||||||
|
});
|
||||||
|
uid = "0";
|
||||||
}
|
}
|
||||||
if (userFound) {
|
if (userFound) {
|
||||||
if (userFound.type === 'email') {
|
if (userFound.type === "email") {
|
||||||
if (userFound.password === 'RESETME') {
|
if (userFound.password === "RESETME") {
|
||||||
const hashedPassword = await hashPassword(password);
|
const hashedPassword = await hashPassword(password);
|
||||||
if (userFound.updatedAt < new Date(Date.now() - 1000 * 60 * 10)) {
|
if (userFound.updatedAt < new Date(Date.now() - 1000 * 60 * 10)) {
|
||||||
if (userFound.id === '0') {
|
if (userFound.id === "0") {
|
||||||
await prisma.user.update({
|
await prisma.user.update({
|
||||||
where: { email: userFound.email },
|
where: { email: userFound.email },
|
||||||
data: { password: 'RESETME' }
|
data: { password: "RESETME" },
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await prisma.user.update({
|
await prisma.user.update({
|
||||||
where: { email: userFound.email },
|
where: { email: userFound.email },
|
||||||
data: { password: 'RESETTIMEOUT' }
|
data: { password: "RESETTIMEOUT" },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
throw {
|
throw {
|
||||||
status: 500,
|
status: 500,
|
||||||
message: 'Password reset link has expired. Please request a new one.'
|
message:
|
||||||
|
"Password reset link has expired. Please request a new one.",
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
await prisma.user.update({
|
await prisma.user.update({
|
||||||
where: { email: userFound.email },
|
where: { email: userFound.email },
|
||||||
data: { password: hashedPassword }
|
data: { password: hashedPassword },
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
userId: userFound.id,
|
userId: userFound.id,
|
||||||
teamId: userFound.id,
|
teamId: userFound.id,
|
||||||
permission: userFound.permission,
|
permission: userFound.permission,
|
||||||
isAdmin: true
|
isAdmin: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const passwordMatch = await bcrypt.compare(password, userFound.password);
|
const passwordMatch = await bcrypt.compare(
|
||||||
|
password,
|
||||||
|
userFound.password
|
||||||
|
);
|
||||||
if (!passwordMatch) {
|
if (!passwordMatch) {
|
||||||
throw {
|
throw {
|
||||||
status: 500,
|
status: 500,
|
||||||
message: 'Wrong password or email address.'
|
message: "Wrong password or email address.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
uid = userFound.id;
|
uid = userFound.id;
|
||||||
isAdmin = true;
|
isAdmin = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
permission = 'owner';
|
permission = "owner";
|
||||||
isAdmin = true;
|
isAdmin = true;
|
||||||
if (!isRegistrationEnabled) {
|
if (!isRegistrationEnabled) {
|
||||||
throw {
|
throw {
|
||||||
status: 404,
|
status: 404,
|
||||||
message: 'Registration disabled by administrator.'
|
message: "Registration disabled by administrator.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const hashedPassword = await hashPassword(password);
|
const hashedPassword = await hashPassword(password);
|
||||||
@@ -221,17 +253,17 @@ export async function login(request: FastifyRequest<Login>, reply: FastifyReply)
|
|||||||
id: uid,
|
id: uid,
|
||||||
email,
|
email,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
type: 'email',
|
type: "email",
|
||||||
teams: {
|
teams: {
|
||||||
create: {
|
create: {
|
||||||
id: uid,
|
id: uid,
|
||||||
name: uniqueName(),
|
name: uniqueName(),
|
||||||
destinationDocker: { connect: { network: 'coolify' } }
|
destinationDocker: { connect: { network: "coolify" } },
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
permission: { create: { teamId: uid, permission: 'owner' } }
|
permission: { create: { teamId: uid, permission: "owner" } },
|
||||||
},
|
},
|
||||||
include: { teams: true }
|
include: { teams: true },
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await prisma.user.create({
|
await prisma.user.create({
|
||||||
@@ -239,16 +271,16 @@ export async function login(request: FastifyRequest<Login>, reply: FastifyReply)
|
|||||||
id: uid,
|
id: uid,
|
||||||
email,
|
email,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
type: 'email',
|
type: "email",
|
||||||
teams: {
|
teams: {
|
||||||
create: {
|
create: {
|
||||||
id: uid,
|
id: uid,
|
||||||
name: uniqueName()
|
name: uniqueName(),
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
permission: { create: { teamId: uid, permission: 'owner' } }
|
permission: { create: { teamId: uid, permission: "owner" } },
|
||||||
},
|
},
|
||||||
include: { teams: true }
|
include: { teams: true },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -256,18 +288,21 @@ export async function login(request: FastifyRequest<Login>, reply: FastifyReply)
|
|||||||
userId: uid,
|
userId: uid,
|
||||||
teamId: uid,
|
teamId: uid,
|
||||||
permission,
|
permission,
|
||||||
isAdmin
|
isAdmin,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCurrentUser(request: FastifyRequest<GetCurrentUser>, fastify) {
|
export async function getCurrentUser(
|
||||||
let token = null
|
request: FastifyRequest<GetCurrentUser>,
|
||||||
const { teamId } = request.query
|
fastify
|
||||||
|
) {
|
||||||
|
let token = null;
|
||||||
|
const { teamId } = request.query;
|
||||||
try {
|
try {
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { id: request.user.userId }
|
where: { id: request.user.userId },
|
||||||
})
|
});
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw "User not found";
|
throw "User not found";
|
||||||
}
|
}
|
||||||
@@ -278,20 +313,20 @@ export async function getCurrentUser(request: FastifyRequest<GetCurrentUser>, fa
|
|||||||
try {
|
try {
|
||||||
const user = await prisma.user.findFirst({
|
const user = await prisma.user.findFirst({
|
||||||
where: { id: request.user.userId, teams: { some: { id: teamId } } },
|
where: { id: request.user.userId, teams: { some: { id: teamId } } },
|
||||||
include: { teams: true, permission: true }
|
include: { teams: true, permission: true },
|
||||||
})
|
});
|
||||||
if (user) {
|
if (user) {
|
||||||
const permission = user.permission.find(p => p.teamId === teamId).permission
|
const permission = user.permission.find(
|
||||||
|
(p) => p.teamId === teamId
|
||||||
|
).permission;
|
||||||
const payload = {
|
const payload = {
|
||||||
...request.user,
|
...request.user,
|
||||||
teamId,
|
teamId,
|
||||||
permission: permission || null,
|
permission: permission || null,
|
||||||
isAdmin: permission === 'owner' || permission === 'admin'
|
isAdmin: permission === "owner" || permission === "admin",
|
||||||
|
};
|
||||||
}
|
token = fastify.jwt.sign(payload);
|
||||||
token = fastify.jwt.sign(payload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// No new token -> not switching teams
|
// No new token -> not switching teams
|
||||||
}
|
}
|
||||||
@@ -300,6 +335,6 @@ export async function getCurrentUser(request: FastifyRequest<GetCurrentUser>, fa
|
|||||||
settings: await prisma.setting.findFirst(),
|
settings: await prisma.setting.findFirst(),
|
||||||
supportedServiceTypesAndVersions,
|
supportedServiceTypesAndVersions,
|
||||||
token,
|
token,
|
||||||
...request.user
|
...request.user,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { FastifyPluginAsync } from 'fastify';
|
import { FastifyPluginAsync } from 'fastify';
|
||||||
import { checkUpdate, login, showDashboard, update, showUsage, getCurrentUser, cleanupManually, restartCoolify } from './handlers';
|
import { checkUpdate, login, showDashboard, update, resetQueue, getCurrentUser, cleanupManually, restartCoolify } from './handlers';
|
||||||
import { GetCurrentUser } from './types';
|
import { GetCurrentUser } from './types';
|
||||||
|
import pump from 'pump'
|
||||||
|
import fs from 'fs'
|
||||||
|
import { asyncExecShell, encrypt, errorHandler, prisma } from '../../../lib/common';
|
||||||
|
|
||||||
export interface Update {
|
export interface Update {
|
||||||
Body: { latestVersion: string }
|
Body: { latestVersion: string }
|
||||||
@@ -23,9 +26,7 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
|||||||
onRequest: [fastify.authenticate]
|
onRequest: [fastify.authenticate]
|
||||||
}, async (request) => await getCurrentUser(request, fastify));
|
}, async (request) => await getCurrentUser(request, fastify));
|
||||||
|
|
||||||
fastify.get('/undead', {
|
fastify.get('/undead', async function () {
|
||||||
onRequest: [fastify.authenticate]
|
|
||||||
}, async function () {
|
|
||||||
return { message: 'nope' };
|
return { message: 'nope' };
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -43,17 +44,17 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
|||||||
onRequest: [fastify.authenticate]
|
onRequest: [fastify.authenticate]
|
||||||
}, async (request) => await showDashboard(request));
|
}, async (request) => await showDashboard(request));
|
||||||
|
|
||||||
fastify.get('/usage', {
|
|
||||||
onRequest: [fastify.authenticate]
|
|
||||||
}, async () => await showUsage());
|
|
||||||
|
|
||||||
fastify.post('/internal/restart', {
|
fastify.post('/internal/restart', {
|
||||||
onRequest: [fastify.authenticate]
|
onRequest: [fastify.authenticate]
|
||||||
}, async (request) => await restartCoolify(request));
|
}, async (request) => await restartCoolify(request));
|
||||||
|
|
||||||
|
fastify.post('/internal/resetQueue', {
|
||||||
|
onRequest: [fastify.authenticate]
|
||||||
|
}, async (request) => await resetQueue(request));
|
||||||
|
|
||||||
fastify.post('/internal/cleanup', {
|
fastify.post('/internal/cleanup', {
|
||||||
onRequest: [fastify.authenticate]
|
onRequest: [fastify.authenticate]
|
||||||
}, async () => await cleanupManually());
|
}, async (request) => await cleanupManually(request));
|
||||||
};
|
};
|
||||||
|
|
||||||
export default root;
|
export default root;
|
||||||
|
|||||||
116
apps/api/src/routes/api/v1/servers/handlers.ts
Normal file
116
apps/api/src/routes/api/v1/servers/handlers.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import type { FastifyRequest } from 'fastify';
|
||||||
|
import { errorHandler, executeDockerCmd, prisma, createRemoteEngineConfiguration, executeSSHCmd } from '../../../../lib/common';
|
||||||
|
import os from 'node:os';
|
||||||
|
import osu from 'node-os-utils';
|
||||||
|
|
||||||
|
|
||||||
|
export async function listServers(request: FastifyRequest) {
|
||||||
|
try {
|
||||||
|
const userId = request.user.userId;
|
||||||
|
const teamId = request.user.teamId;
|
||||||
|
const servers = await prisma.destinationDocker.findMany({ where: { teams: { some: { id: teamId === '0' ? undefined : teamId } }}, distinct: ['remoteIpAddress', 'engine'] })
|
||||||
|
return {
|
||||||
|
servers
|
||||||
|
}
|
||||||
|
} catch ({ status, message }) {
|
||||||
|
return errorHandler({ status, message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const mappingTable = [
|
||||||
|
['K total memory', 'totalMemoryKB'],
|
||||||
|
['K used memory', 'usedMemoryKB'],
|
||||||
|
['K active memory', 'activeMemoryKB'],
|
||||||
|
['K inactive memory', 'inactiveMemoryKB'],
|
||||||
|
['K free memory', 'freeMemoryKB'],
|
||||||
|
['K buffer memory', 'bufferMemoryKB'],
|
||||||
|
['K swap cache', 'swapCacheKB'],
|
||||||
|
['K total swap', 'totalSwapKB'],
|
||||||
|
['K used swap', 'usedSwapKB'],
|
||||||
|
['K free swap', 'freeSwapKB'],
|
||||||
|
['non-nice user cpu ticks', 'nonNiceUserCpuTicks'],
|
||||||
|
['nice user cpu ticks', 'niceUserCpuTicks'],
|
||||||
|
['system cpu ticks', 'systemCpuTicks'],
|
||||||
|
['idle cpu ticks', 'idleCpuTicks'],
|
||||||
|
['IO-wait cpu ticks', 'ioWaitCpuTicks'],
|
||||||
|
['IRQ cpu ticks', 'irqCpuTicks'],
|
||||||
|
['softirq cpu ticks', 'softIrqCpuTicks'],
|
||||||
|
['stolen cpu ticks', 'stolenCpuTicks'],
|
||||||
|
['pages paged in', 'pagesPagedIn'],
|
||||||
|
['pages paged out', 'pagesPagedOut'],
|
||||||
|
['pages swapped in', 'pagesSwappedIn'],
|
||||||
|
['pages swapped out', 'pagesSwappedOut'],
|
||||||
|
['interrupts', 'interrupts'],
|
||||||
|
['CPU context switches', 'cpuContextSwitches'],
|
||||||
|
['boot time', 'bootTime'],
|
||||||
|
['forks', 'forks']
|
||||||
|
];
|
||||||
|
function parseFromText(text) {
|
||||||
|
var data = {};
|
||||||
|
var lines = text.split(/\r?\n/);
|
||||||
|
for (const line of lines) {
|
||||||
|
for (const [key, value] of mappingTable) {
|
||||||
|
if (line.indexOf(key) >= 0) {
|
||||||
|
const values = line.match(/[0-9]+/)[0];
|
||||||
|
data[value] = parseInt(values, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
export async function showUsage(request: FastifyRequest) {
|
||||||
|
const { id } = request.params;
|
||||||
|
let { remoteEngine } = request.query
|
||||||
|
remoteEngine = remoteEngine === 'true' ? true : false
|
||||||
|
if (remoteEngine) {
|
||||||
|
const { stdout: stats } = await executeSSHCmd({ dockerId: id, command: `vmstat -s` })
|
||||||
|
const { stdout: disks } = await executeSSHCmd({ dockerId: id, command: `df -m / --output=size,used,pcent|grep -v 'Used'| xargs` })
|
||||||
|
const { stdout: cpus } = await executeSSHCmd({ dockerId: id, command: `nproc --all` })
|
||||||
|
const { stdout: cpuUsage } = await executeSSHCmd({ dockerId: id, command: `echo $[100-$(vmstat 1 2|tail -1|awk '{print $15}')]` })
|
||||||
|
const parsed: any = parseFromText(stats)
|
||||||
|
return {
|
||||||
|
usage: {
|
||||||
|
uptime: parsed.bootTime / 1024,
|
||||||
|
memory: {
|
||||||
|
totalMemMb: parsed.totalMemoryKB / 1024,
|
||||||
|
usedMemMb: parsed.usedMemoryKB / 1024,
|
||||||
|
freeMemMb: parsed.freeMemoryKB / 1024,
|
||||||
|
usedMemPercentage: (parsed.usedMemoryKB / parsed.totalMemoryKB) * 100,
|
||||||
|
freeMemPercentage: (parsed.totalMemoryKB - parsed.usedMemoryKB) / parsed.totalMemoryKB * 100
|
||||||
|
},
|
||||||
|
cpu: {
|
||||||
|
load: [0,0,0],
|
||||||
|
usage: cpuUsage,
|
||||||
|
count: cpus
|
||||||
|
},
|
||||||
|
disk: {
|
||||||
|
totalGb: (disks.split(' ')[0] / 1024).toFixed(1),
|
||||||
|
usedGb: (disks.split(' ')[1] / 1024).toFixed(1),
|
||||||
|
freeGb: (disks.split(' ')[0] - disks.split(' ')[1]).toFixed(1),
|
||||||
|
usedPercentage: disks.split(' ')[2].replace('%', ''),
|
||||||
|
freePercentage: 100 - disks.split(' ')[2].replace('%', '')
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
usage: {
|
||||||
|
uptime: os.uptime(),
|
||||||
|
memory: await osu.mem.info(),
|
||||||
|
cpu: {
|
||||||
|
load: os.loadavg(),
|
||||||
|
usage: await osu.cpu.usage(),
|
||||||
|
count: os.cpus().length
|
||||||
|
},
|
||||||
|
disk: await osu.drive.info('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
} catch ({ status, message }) {
|
||||||
|
return errorHandler({ status, message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
14
apps/api/src/routes/api/v1/servers/index.ts
Normal file
14
apps/api/src/routes/api/v1/servers/index.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { FastifyPluginAsync } from 'fastify';
|
||||||
|
import { listServers, showUsage } from './handlers';
|
||||||
|
|
||||||
|
|
||||||
|
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||||
|
fastify.addHook('onRequest', async (request) => {
|
||||||
|
return await request.jwtVerify()
|
||||||
|
})
|
||||||
|
fastify.get('/', async (request) => await listServers(request));
|
||||||
|
fastify.get('/usage/:id', async (request) => await showUsage(request));
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export default root;
|
||||||
27
apps/api/src/routes/api/v1/servers/types.ts
Normal file
27
apps/api/src/routes/api/v1/servers/types.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { OnlyId } from "../../../../types"
|
||||||
|
|
||||||
|
export interface SaveTeam extends OnlyId {
|
||||||
|
Body: {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export interface InviteToTeam {
|
||||||
|
Body: {
|
||||||
|
email: string,
|
||||||
|
permission: string,
|
||||||
|
teamId: string,
|
||||||
|
teamName: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export interface BodyId {
|
||||||
|
Body: {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export interface SetPermission {
|
||||||
|
Body: {
|
||||||
|
userId: string,
|
||||||
|
newPermission: string,
|
||||||
|
permissionId: string
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import { prisma, uniqueName, asyncExecShell, getServiceFromDB, getContainerUsage, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, ComposeFile, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, executeDockerCmd, checkDomainsIsValidInDNS, checkExposedPort } from '../../../../lib/common';
|
import { prisma, uniqueName, asyncExecShell, getServiceFromDB, getContainerUsage, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, ComposeFile, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, executeDockerCmd, checkDomainsIsValidInDNS, checkExposedPort, listSettings } from '../../../../lib/common';
|
||||||
import { day } from '../../../../lib/dayjs';
|
import { day } from '../../../../lib/dayjs';
|
||||||
import { checkContainer, isContainerExited } from '../../../../lib/docker';
|
import { checkContainer, isContainerExited } from '../../../../lib/docker';
|
||||||
import cuid from 'cuid';
|
import cuid from 'cuid';
|
||||||
@@ -43,13 +43,17 @@ export async function getServiceStatus(request: FastifyRequest<OnlyId>) {
|
|||||||
|
|
||||||
let isRunning = false;
|
let isRunning = false;
|
||||||
let isExited = false
|
let isExited = false
|
||||||
|
let isRestarting = false;
|
||||||
const service = await getServiceFromDB({ id, teamId });
|
const service = await getServiceFromDB({ id, teamId });
|
||||||
const { destinationDockerId, settings } = service;
|
const { destinationDockerId, settings } = service;
|
||||||
|
|
||||||
if (destinationDockerId) {
|
if (destinationDockerId) {
|
||||||
isRunning = await checkContainer({ dockerId: service.destinationDocker.id, container: id });
|
const status = await checkContainer({ dockerId: service.destinationDocker.id, container: id });
|
||||||
isExited = await isContainerExited(service.destinationDocker.id, id);
|
if (status?.found) {
|
||||||
|
isRunning = status.status.isRunning;
|
||||||
|
isExited = status.status.isExited;
|
||||||
|
isRestarting = status.status.isRestarting
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
isRunning,
|
isRunning,
|
||||||
@@ -70,6 +74,7 @@ export async function getService(request: FastifyRequest<OnlyId>) {
|
|||||||
throw { status: 404, message: 'Service not found.' }
|
throw { status: 404, message: 'Service not found.' }
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
settings: await listSettings(),
|
||||||
service
|
service
|
||||||
}
|
}
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
@@ -232,7 +237,7 @@ export async function checkService(request: FastifyRequest<CheckService>) {
|
|||||||
if (otherFqdns && otherFqdns.length > 0) otherFqdns = otherFqdns.map((f) => f.toLowerCase());
|
if (otherFqdns && otherFqdns.length > 0) otherFqdns = otherFqdns.map((f) => f.toLowerCase());
|
||||||
if (exposePort) exposePort = Number(exposePort);
|
if (exposePort) exposePort = Number(exposePort);
|
||||||
|
|
||||||
const { destinationDocker: { id: dockerId, remoteIpAddress, remoteEngine }, exposePort: configuredPort } = await prisma.service.findUnique({ where: { id }, include: { destinationDocker: true } })
|
const { destinationDocker: { remoteIpAddress, remoteEngine, engine }, exposePort: configuredPort } = await prisma.service.findUnique({ where: { id }, include: { destinationDocker: true } })
|
||||||
const { isDNSCheckEnabled } = await prisma.setting.findFirst({});
|
const { isDNSCheckEnabled } = await prisma.setting.findFirst({});
|
||||||
|
|
||||||
let found = await isDomainConfigured({ id, fqdn, remoteIpAddress });
|
let found = await isDomainConfigured({ id, fqdn, remoteIpAddress });
|
||||||
@@ -247,7 +252,7 @@ export async function checkService(request: FastifyRequest<CheckService>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, dockerId, remoteIpAddress })
|
if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress })
|
||||||
if (isDNSCheckEnabled && !isDev && !forceSave) {
|
if (isDNSCheckEnabled && !isDev && !forceSave) {
|
||||||
let hostname = request.hostname.split(':')[0];
|
let hostname = request.hostname.split(':')[0];
|
||||||
if (remoteEngine) hostname = remoteIpAddress;
|
if (remoteEngine) hostname = remoteIpAddress;
|
||||||
@@ -451,7 +456,7 @@ export async function activatePlausibleUsers(request: FastifyRequest<OnlyId>, re
|
|||||||
if (destinationDockerId) {
|
if (destinationDockerId) {
|
||||||
await executeDockerCmd({
|
await executeDockerCmd({
|
||||||
dockerId: destinationDocker.id,
|
dockerId: destinationDocker.id,
|
||||||
command: `docker exec ${id} 'psql -H postgresql://${postgresqlUser}:${postgresqlPassword}@localhost:5432/${postgresqlDatabase} -c "UPDATE users SET email_verified = true;"'`
|
command: `docker exec ${id}-postgresql psql -H postgresql://${postgresqlUser}:${postgresqlPassword}@localhost:5432/${postgresqlDatabase} -c "UPDATE users SET email_verified = true;"`
|
||||||
})
|
})
|
||||||
return await reply.code(201).send()
|
return await reply.code(201).send()
|
||||||
}
|
}
|
||||||
@@ -471,7 +476,7 @@ export async function cleanupPlausibleLogs(request: FastifyRequest<OnlyId>, repl
|
|||||||
if (destinationDockerId) {
|
if (destinationDockerId) {
|
||||||
await executeDockerCmd({
|
await executeDockerCmd({
|
||||||
dockerId: destinationDocker.id,
|
dockerId: destinationDocker.id,
|
||||||
command: `docker exec ${id}-clickhouse sh -c "/usr/bin/clickhouse-client -q \\"SELECT name FROM system.tables WHERE name LIKE '%log%';\\"| xargs -I{} /usr/bin/clickhouse-client -q \"TRUNCATE TABLE system.{};\""`
|
command: `docker exec ${id}-clickhouse /usr/bin/clickhouse-client -q \\"SELECT name FROM system.tables WHERE name LIKE '%log%';\\"| xargs -I{} /usr/bin/clickhouse-client -q \"TRUNCATE TABLE system.{};\"`
|
||||||
})
|
})
|
||||||
return await reply.code(201).send()
|
return await reply.code(201).send()
|
||||||
}
|
}
|
||||||
@@ -484,9 +489,9 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
|
|||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
const { ftpEnabled } = request.body;
|
const { ftpEnabled } = request.body;
|
||||||
|
|
||||||
const { service: { destinationDocker: { id: dockerId } } } = await prisma.wordpress.findUnique({ where: { serviceId: id }, include: { service: { include: { destinationDocker: true } } } })
|
const { service: { destinationDocker: { engine, remoteEngine, remoteIpAddress } } } = await prisma.wordpress.findUnique({ where: { serviceId: id }, include: { service: { include: { destinationDocker: true } } } })
|
||||||
|
|
||||||
const publicPort = await getFreePublicPort(id, dockerId);
|
const publicPort = await getFreePublicPort({ id, remoteEngine, engine, remoteIpAddress });
|
||||||
|
|
||||||
let ftpUser = cuid();
|
let ftpUser = cuid();
|
||||||
let ftpPassword = generatePassword({});
|
let ftpPassword = generatePassword({});
|
||||||
@@ -553,7 +558,7 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const isRunning = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-ftp` });
|
const { found: isRunning } = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-ftp` });
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
await executeDockerCmd({
|
await executeDockerCmd({
|
||||||
dockerId: destinationDocker.id,
|
dockerId: destinationDocker.id,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
|
|
||||||
import type { OnlyId } from '../../../../types';
|
import type { OnlyId } from '../../../../types';
|
||||||
import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetGlitchTipSettings, SetWordpressSettings } from './types';
|
import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetGlitchTipSettings, SetWordpressSettings } from './types';
|
||||||
import { startService, stopService } from '../../../../lib/services/handlers';
|
import { migrateAppwriteDB, startService, stopService } from '../../../../lib/services/handlers';
|
||||||
|
|
||||||
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||||
fastify.addHook('onRequest', async (request) => {
|
fastify.addHook('onRequest', async (request) => {
|
||||||
@@ -76,6 +76,8 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
|||||||
fastify.post<OnlyId>('/:id/plausibleanalytics/activate', async (request, reply) => await activatePlausibleUsers(request, reply));
|
fastify.post<OnlyId>('/:id/plausibleanalytics/activate', async (request, reply) => await activatePlausibleUsers(request, reply));
|
||||||
fastify.post<OnlyId>('/:id/plausibleanalytics/cleanup', async (request, reply) => await cleanupPlausibleLogs(request, reply));
|
fastify.post<OnlyId>('/:id/plausibleanalytics/cleanup', async (request, reply) => await cleanupPlausibleLogs(request, reply));
|
||||||
fastify.post<ActivateWordpressFtp>('/:id/wordpress/ftp', async (request, reply) => await activateWordpressFtp(request, reply));
|
fastify.post<ActivateWordpressFtp>('/:id/wordpress/ftp', async (request, reply) => await activateWordpressFtp(request, reply));
|
||||||
|
|
||||||
|
fastify.post<OnlyId>('/:id/appwrite/migrate', async (request, reply) => await migrateAppwriteDB(request, reply));
|
||||||
};
|
};
|
||||||
|
|
||||||
export default root;
|
export default root;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { promises as dns } from 'dns';
|
import { promises as dns } from 'dns';
|
||||||
|
import { X509Certificate } from 'node:crypto';
|
||||||
|
|
||||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||||
import { checkDomainsIsValidInDNS, decrypt, encrypt, errorHandler, getDomain, isDNSValid, isDomainConfigured, listSettings, prisma } from '../../../../lib/common';
|
import { asyncExecShell, checkDomainsIsValidInDNS, decrypt, encrypt, errorHandler, isDNSValid, isDomainConfigured, listSettings, prisma } from '../../../../lib/common';
|
||||||
import { CheckDNS, CheckDomain, DeleteDomain, DeleteSSHKey, SaveSettings, SaveSSHKey } from './types';
|
import { CheckDNS, CheckDomain, DeleteDomain, OnlyIdInBody, SaveSettings, SaveSSHKey } from './types';
|
||||||
|
|
||||||
|
|
||||||
export async function listAllSettings(request: FastifyRequest) {
|
export async function listAllSettings(request: FastifyRequest) {
|
||||||
@@ -16,8 +17,16 @@ export async function listAllSettings(request: FastifyRequest) {
|
|||||||
unencryptedKeys.push({ id: key.id, name: key.name, privateKey: decrypt(key.privateKey), createdAt: key.createdAt })
|
unencryptedKeys.push({ id: key.id, name: key.name, privateKey: decrypt(key.privateKey), createdAt: key.createdAt })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const certificates = await prisma.certificate.findMany({ where: { team: { id: teamId } } })
|
||||||
|
let cns = [];
|
||||||
|
for (const certificate of certificates) {
|
||||||
|
const x509 = new X509Certificate(certificate.cert);
|
||||||
|
cns.push({ commonName: x509.subject.split('\n').find((s) => s.startsWith('CN=')).replace('CN=', ''), id: certificate.id, createdAt: certificate.createdAt })
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
settings,
|
settings,
|
||||||
|
certificates: cns,
|
||||||
sshKeys: unencryptedKeys
|
sshKeys: unencryptedKeys
|
||||||
}
|
}
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
@@ -58,7 +67,7 @@ export async function deleteDomain(request: FastifyRequest<DeleteDomain>, reply:
|
|||||||
const { fqdn } = request.body
|
const { fqdn } = request.body
|
||||||
const { DNSServers } = await listSettings();
|
const { DNSServers } = await listSettings();
|
||||||
if (DNSServers) {
|
if (DNSServers) {
|
||||||
dns.setServers([DNSServers]);
|
dns.setServers([...DNSServers.split(',')]);
|
||||||
}
|
}
|
||||||
let ip;
|
let ip;
|
||||||
try {
|
try {
|
||||||
@@ -118,7 +127,7 @@ export async function saveSSHKey(request: FastifyRequest<SaveSSHKey>, reply: Fas
|
|||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export async function deleteSSHKey(request: FastifyRequest<DeleteSSHKey>, reply: FastifyReply) {
|
export async function deleteSSHKey(request: FastifyRequest<OnlyIdInBody>, reply: FastifyReply) {
|
||||||
try {
|
try {
|
||||||
const { id } = request.body;
|
const { id } = request.body;
|
||||||
await prisma.sshKey.delete({ where: { id } })
|
await prisma.sshKey.delete({ where: { id } })
|
||||||
@@ -126,4 +135,15 @@ export async function deleteSSHKey(request: FastifyRequest<DeleteSSHKey>, reply:
|
|||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteCertificates(request: FastifyRequest<OnlyIdInBody>, reply: FastifyReply) {
|
||||||
|
try {
|
||||||
|
const { id } = request.body;
|
||||||
|
await asyncExecShell(`docker exec coolify-proxy sh -c 'rm -f /etc/traefik/acme/custom/${id}-key.pem /etc/traefik/acme/custom/${id}-cert.pem'`)
|
||||||
|
await prisma.certificate.delete({ where: { id } })
|
||||||
|
return reply.code(201).send()
|
||||||
|
} catch ({ status, message }) {
|
||||||
|
return errorHandler({ status, message })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,59 @@
|
|||||||
import { FastifyPluginAsync } from 'fastify';
|
import { FastifyPluginAsync } from 'fastify';
|
||||||
import { checkDNS, checkDomain, deleteDomain, deleteSSHKey, listAllSettings, saveSettings, saveSSHKey } from './handlers';
|
import { X509Certificate } from 'node:crypto';
|
||||||
import { CheckDNS, CheckDomain, DeleteDomain, DeleteSSHKey, SaveSettings, SaveSSHKey } from './types';
|
|
||||||
|
import { encrypt, errorHandler, prisma } from '../../../../lib/common';
|
||||||
|
import { checkDNS, checkDomain, deleteCertificates, deleteDomain, deleteSSHKey, listAllSettings, saveSettings, saveSSHKey } from './handlers';
|
||||||
|
import { CheckDNS, CheckDomain, DeleteDomain, OnlyIdInBody, SaveSettings, SaveSSHKey } from './types';
|
||||||
|
|
||||||
|
|
||||||
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||||
fastify.addHook('onRequest', async (request) => {
|
fastify.addHook('onRequest', async (request) => {
|
||||||
return await request.jwtVerify()
|
return await request.jwtVerify()
|
||||||
})
|
})
|
||||||
fastify.get('/', async (request) => await listAllSettings(request));
|
fastify.get('/', async (request) => await listAllSettings(request));
|
||||||
fastify.post<SaveSettings>('/', async (request, reply) => await saveSettings(request, reply));
|
fastify.post<SaveSettings>('/', async (request, reply) => await saveSettings(request, reply));
|
||||||
fastify.delete<DeleteDomain>('/', async (request, reply) => await deleteDomain(request, reply));
|
fastify.delete<DeleteDomain>('/', async (request, reply) => await deleteDomain(request, reply));
|
||||||
|
|
||||||
fastify.get<CheckDNS>('/check', async (request) => await checkDNS(request));
|
fastify.get<CheckDNS>('/check', async (request) => await checkDNS(request));
|
||||||
fastify.post<CheckDomain>('/check', async (request) => await checkDomain(request));
|
fastify.post<CheckDomain>('/check', async (request) => await checkDomain(request));
|
||||||
|
|
||||||
fastify.post<SaveSSHKey>('/sshKey', async (request, reply) => await saveSSHKey(request, reply));
|
fastify.post<SaveSSHKey>('/sshKey', async (request, reply) => await saveSSHKey(request, reply));
|
||||||
fastify.delete<DeleteSSHKey>('/sshKey', async (request, reply) => await deleteSSHKey(request, reply));
|
fastify.delete<OnlyIdInBody>('/sshKey', async (request, reply) => await deleteSSHKey(request, reply));
|
||||||
|
|
||||||
|
fastify.post('/upload', async (request) => {
|
||||||
|
try {
|
||||||
|
const teamId = request.user.teamId;
|
||||||
|
const certificates = await prisma.certificate.findMany({})
|
||||||
|
let cns = [];
|
||||||
|
for (const certificate of certificates) {
|
||||||
|
const x509 = new X509Certificate(certificate.cert);
|
||||||
|
cns.push(x509.subject.split('\n').find((s) => s.startsWith('CN=')).replace('CN=', ''))
|
||||||
|
}
|
||||||
|
const parts = await request.files()
|
||||||
|
let key = null
|
||||||
|
let cert = null
|
||||||
|
for await (const part of parts) {
|
||||||
|
const name = part.fieldname
|
||||||
|
if (name === 'key') key = (await part.toBuffer()).toString()
|
||||||
|
if (name === 'cert') cert = (await part.toBuffer()).toString()
|
||||||
|
}
|
||||||
|
const x509 = new X509Certificate(cert);
|
||||||
|
const cn = x509.subject.split('\n').find((s) => s.startsWith('CN=')).replace('CN=', '')
|
||||||
|
if (cns.includes(cn)) {
|
||||||
|
throw {
|
||||||
|
message: `A certificate with ${cn} common name already exists.`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await prisma.certificate.create({ data: { cert, key: encrypt(key), team: { connect: { id: teamId } } } })
|
||||||
|
await prisma.applicationSettings.updateMany({ where: { application: { AND: [{ fqdn: { endsWith: cn } }, { fqdn: { startsWith: 'https' } }] } }, data: { isCustomSSL: true } })
|
||||||
|
return { message: 'Certificated uploaded' }
|
||||||
|
} catch ({ status, message }) {
|
||||||
|
return errorHandler({ status, message });
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
fastify.delete<OnlyIdInBody>('/certificate', async (request, reply) => await deleteCertificates(request, reply))
|
||||||
|
// fastify.get('/certificates', async (request) => await getCertificates(request))
|
||||||
};
|
};
|
||||||
|
|
||||||
export default root;
|
export default root;
|
||||||
|
|||||||
@@ -41,4 +41,9 @@ export interface DeleteSSHKey {
|
|||||||
Body: {
|
Body: {
|
||||||
id: string
|
id: string
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
export interface OnlyIdInBody {
|
||||||
|
Body: {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import cuid from "cuid";
|
import cuid from "cuid";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import { encrypt, errorHandler, getUIUrl, isDev, prisma } from "../../../lib/common";
|
import { encrypt, errorHandler, getDomain, getUIUrl, isDev, prisma } from "../../../lib/common";
|
||||||
import { checkContainer, removeContainer } from "../../../lib/docker";
|
import { checkContainer, removeContainer } from "../../../lib/docker";
|
||||||
import { createdBranchDatabase, getApplicationFromDBWebhook, removeBranchDatabase } from "../../api/v1/applications/handlers";
|
import { createdBranchDatabase, getApplicationFromDBWebhook, removeBranchDatabase } from "../../api/v1/applications/handlers";
|
||||||
|
|
||||||
@@ -154,7 +154,7 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
|
|||||||
|
|
||||||
if (application.settings.previews) {
|
if (application.settings.previews) {
|
||||||
if (application.destinationDockerId) {
|
if (application.destinationDockerId) {
|
||||||
const isRunning = await checkContainer(
|
const { found: isRunning } = await checkContainer(
|
||||||
{
|
{
|
||||||
dockerId: application.destinationDocker.id,
|
dockerId: application.destinationDocker.id,
|
||||||
container: application.id
|
container: application.id
|
||||||
@@ -169,24 +169,44 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
|
|||||||
pullmergeRequestAction === 'reopened' ||
|
pullmergeRequestAction === 'reopened' ||
|
||||||
pullmergeRequestAction === 'synchronize'
|
pullmergeRequestAction === 'synchronize'
|
||||||
) {
|
) {
|
||||||
|
|
||||||
await prisma.application.update({
|
await prisma.application.update({
|
||||||
where: { id: application.id },
|
where: { id: application.id },
|
||||||
data: { updatedAt: new Date() }
|
data: { updatedAt: new Date() }
|
||||||
});
|
});
|
||||||
if (application.connectedDatabase && pullmergeRequestAction === 'opened' || pullmergeRequestAction === 'reopened') {
|
let previewApplicationId = undefined
|
||||||
// Coolify hosted database
|
if (pullmergeRequestId) {
|
||||||
if (application.connectedDatabase.databaseId) {
|
const foundPreviewApplications = await prisma.previewApplication.findMany({ where: { applicationId: application.id, pullmergeRequestId } })
|
||||||
const databaseId = application.connectedDatabase.databaseId;
|
if (foundPreviewApplications.length > 0) {
|
||||||
const database = await prisma.database.findUnique({ where: { id: databaseId } });
|
previewApplicationId = foundPreviewApplications[0].id
|
||||||
if (database) {
|
} else {
|
||||||
await createdBranchDatabase(database, application.connectedDatabase.hostedDatabaseDBName, pullmergeRequestId);
|
const protocol = application.fqdn.includes('https://') ? 'https://' : 'http://'
|
||||||
}
|
const previewApplication = await prisma.previewApplication.create({
|
||||||
|
data: {
|
||||||
|
pullmergeRequestId,
|
||||||
|
sourceBranch,
|
||||||
|
customDomain: `${protocol}${pullmergeRequestId}.${getDomain(application.fqdn)}`,
|
||||||
|
application: { connect: { id: application.id } }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
previewApplicationId = previewApplication.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// if (application.connectedDatabase && pullmergeRequestAction === 'opened' || pullmergeRequestAction === 'reopened') {
|
||||||
|
// // Coolify hosted database
|
||||||
|
// if (application.connectedDatabase.databaseId) {
|
||||||
|
// const databaseId = application.connectedDatabase.databaseId;
|
||||||
|
// const database = await prisma.database.findUnique({ where: { id: databaseId } });
|
||||||
|
// if (database) {
|
||||||
|
// await createdBranchDatabase(database, application.connectedDatabase.hostedDatabaseDBName, pullmergeRequestId);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
await prisma.build.create({
|
await prisma.build.create({
|
||||||
data: {
|
data: {
|
||||||
id: buildId,
|
id: buildId,
|
||||||
pullmergeRequestId,
|
pullmergeRequestId,
|
||||||
|
previewApplicationId,
|
||||||
sourceBranch,
|
sourceBranch,
|
||||||
applicationId: application.id,
|
applicationId: application.id,
|
||||||
destinationDockerId: application.destinationDocker.id,
|
destinationDockerId: application.destinationDocker.id,
|
||||||
@@ -198,7 +218,9 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'Queued. Thank you!'
|
||||||
|
};
|
||||||
} else if (pullmergeRequestAction === 'closed') {
|
} else if (pullmergeRequestAction === 'closed') {
|
||||||
if (application.destinationDockerId) {
|
if (application.destinationDockerId) {
|
||||||
const id = `${application.id}-${pullmergeRequestId}`;
|
const id = `${application.id}-${pullmergeRequestId}`;
|
||||||
@@ -206,13 +228,22 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
|
|||||||
await removeContainer({ id, dockerId: application.destinationDocker.id });
|
await removeContainer({ id, dockerId: application.destinationDocker.id });
|
||||||
} catch (error) { }
|
} catch (error) { }
|
||||||
}
|
}
|
||||||
if (application.connectedDatabase.databaseId) {
|
const foundPreviewApplications = await prisma.previewApplication.findMany({ where: { applicationId: application.id, pullmergeRequestId } })
|
||||||
const databaseId = application.connectedDatabase.databaseId;
|
if (foundPreviewApplications.length > 0) {
|
||||||
const database = await prisma.database.findUnique({ where: { id: databaseId } });
|
for (const preview of foundPreviewApplications) {
|
||||||
if (database) {
|
await prisma.previewApplication.delete({ where: { id: preview.id } })
|
||||||
await removeBranchDatabase(database, pullmergeRequestId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
message: 'PR closed. Thank you!'
|
||||||
|
};
|
||||||
|
// if (application?.connectedDatabase?.databaseId) {
|
||||||
|
// const databaseId = application.connectedDatabase.databaseId;
|
||||||
|
// const database = await prisma.database.findUnique({ where: { id: databaseId } });
|
||||||
|
// if (database) {
|
||||||
|
// await removeBranchDatabase(database, pullmergeRequestId);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import axios from "axios";
|
|||||||
import cuid from "cuid";
|
import cuid from "cuid";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import type { FastifyReply, FastifyRequest } from "fastify";
|
import type { FastifyReply, FastifyRequest } from "fastify";
|
||||||
import { errorHandler, getAPIUrl, getUIUrl, isDev, listSettings, prisma } from "../../../lib/common";
|
import { errorHandler, getAPIUrl, getDomain, getUIUrl, isDev, listSettings, prisma } from "../../../lib/common";
|
||||||
import { checkContainer, removeContainer } from "../../../lib/docker";
|
import { checkContainer, removeContainer } from "../../../lib/docker";
|
||||||
import { getApplicationFromDB, getApplicationFromDBWebhook } from "../../api/v1/applications/handlers";
|
import { getApplicationFromDB, getApplicationFromDBWebhook } from "../../api/v1/applications/handlers";
|
||||||
|
|
||||||
@@ -91,8 +91,8 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (objectKind === 'merge_request') {
|
} else if (objectKind === 'merge_request') {
|
||||||
const { object_attributes: { work_in_progress: isDraft, action, source_branch: sourceBranch, target_branch: targetBranch, iid: pullmergeRequestId }, project: { id } } = request.body
|
const { object_attributes: { work_in_progress: isDraft, action, source_branch: sourceBranch, target_branch: targetBranch }, project: { id } } = request.body
|
||||||
|
const pullmergeRequestId = request.body.object_attributes.iid.toString();
|
||||||
const projectId = Number(id);
|
const projectId = Number(id);
|
||||||
if (!allowedActions.includes(action)) {
|
if (!allowedActions.includes(action)) {
|
||||||
throw { status: 500, message: 'Action not allowed.' }
|
throw { status: 500, message: 'Action not allowed.' }
|
||||||
@@ -107,7 +107,7 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
|
|||||||
const buildId = cuid();
|
const buildId = cuid();
|
||||||
if (application.settings.previews) {
|
if (application.settings.previews) {
|
||||||
if (application.destinationDockerId) {
|
if (application.destinationDockerId) {
|
||||||
const isRunning = await checkContainer(
|
const { found: isRunning } = await checkContainer(
|
||||||
{
|
{
|
||||||
dockerId: application.destinationDocker.id,
|
dockerId: application.destinationDocker.id,
|
||||||
container: application.id
|
container: application.id
|
||||||
@@ -130,10 +130,29 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
|
|||||||
where: { id: application.id },
|
where: { id: application.id },
|
||||||
data: { updatedAt: new Date() }
|
data: { updatedAt: new Date() }
|
||||||
});
|
});
|
||||||
|
let previewApplicationId = undefined
|
||||||
|
if (pullmergeRequestId) {
|
||||||
|
const foundPreviewApplications = await prisma.previewApplication.findMany({ where: { applicationId: application.id, pullmergeRequestId } })
|
||||||
|
if (foundPreviewApplications.length > 0) {
|
||||||
|
previewApplicationId = foundPreviewApplications[0].id
|
||||||
|
} else {
|
||||||
|
const protocol = application.fqdn.includes('https://') ? 'https://' : 'http://'
|
||||||
|
const previewApplication = await prisma.previewApplication.create({
|
||||||
|
data: {
|
||||||
|
pullmergeRequestId,
|
||||||
|
sourceBranch,
|
||||||
|
customDomain: `${protocol}${pullmergeRequestId}.${getDomain(application.fqdn)}`,
|
||||||
|
application: { connect: { id: application.id } }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
previewApplicationId = previewApplication.id
|
||||||
|
}
|
||||||
|
}
|
||||||
await prisma.build.create({
|
await prisma.build.create({
|
||||||
data: {
|
data: {
|
||||||
id: buildId,
|
id: buildId,
|
||||||
pullmergeRequestId: pullmergeRequestId.toString(),
|
pullmergeRequestId,
|
||||||
|
previewApplicationId,
|
||||||
sourceBranch,
|
sourceBranch,
|
||||||
applicationId: application.id,
|
applicationId: application.id,
|
||||||
destinationDockerId: application.destinationDocker.id,
|
destinationDockerId: application.destinationDocker.id,
|
||||||
@@ -150,8 +169,19 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
|
|||||||
} else if (action === 'close') {
|
} else if (action === 'close') {
|
||||||
if (application.destinationDockerId) {
|
if (application.destinationDockerId) {
|
||||||
const id = `${application.id}-${pullmergeRequestId}`;
|
const id = `${application.id}-${pullmergeRequestId}`;
|
||||||
await removeContainer({ id, dockerId: application.destinationDocker.id });
|
try {
|
||||||
|
await removeContainer({ id, dockerId: application.destinationDocker.id });
|
||||||
|
} catch (error) { }
|
||||||
}
|
}
|
||||||
|
const foundPreviewApplications = await prisma.previewApplication.findMany({ where: { applicationId: application.id, pullmergeRequestId } })
|
||||||
|
if (foundPreviewApplications.length > 0) {
|
||||||
|
for (const preview of foundPreviewApplications) {
|
||||||
|
await prisma.previewApplication.delete({ where: { id: preview.id } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
message: 'MR closed. Thank you!'
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import { TraefikOtherConfiguration } from "./types";
|
|||||||
import { OnlyId } from "../../../types";
|
import { OnlyId } from "../../../types";
|
||||||
|
|
||||||
function configureMiddleware(
|
function configureMiddleware(
|
||||||
{ id, container, port, domain, nakedDomain, isHttps, isWWW, isDualCerts, scriptName, type },
|
{ id, container, port, domain, nakedDomain, isHttps, isWWW, isDualCerts, scriptName, type, isCustomSSL },
|
||||||
traefik
|
traefik
|
||||||
) {
|
) {
|
||||||
if (isHttps) {
|
if (isHttps) {
|
||||||
traefik.http.routers[id] = {
|
traefik.http.routers[id] = {
|
||||||
entrypoints: ['web'],
|
entrypoints: ['web'],
|
||||||
rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`,
|
rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/\`)`,
|
||||||
service: `${id}`,
|
service: `${id}`,
|
||||||
middlewares: ['redirect-to-https']
|
middlewares: ['redirect-to-https']
|
||||||
};
|
};
|
||||||
@@ -53,9 +53,9 @@ function configureMiddleware(
|
|||||||
if (isDualCerts) {
|
if (isDualCerts) {
|
||||||
traefik.http.routers[`${id}-secure`] = {
|
traefik.http.routers[`${id}-secure`] = {
|
||||||
entrypoints: ['websecure'],
|
entrypoints: ['websecure'],
|
||||||
rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`,
|
rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/\`)`,
|
||||||
service: `${id}`,
|
service: `${id}`,
|
||||||
tls: {
|
tls: isCustomSSL ? true : {
|
||||||
certresolver: 'letsencrypt'
|
certresolver: 'letsencrypt'
|
||||||
},
|
},
|
||||||
middlewares: []
|
middlewares: []
|
||||||
@@ -64,16 +64,16 @@ function configureMiddleware(
|
|||||||
if (isWWW) {
|
if (isWWW) {
|
||||||
traefik.http.routers[`${id}-secure-www`] = {
|
traefik.http.routers[`${id}-secure-www`] = {
|
||||||
entrypoints: ['websecure'],
|
entrypoints: ['websecure'],
|
||||||
rule: `Host(\`www.${nakedDomain}\`)`,
|
rule: `Host(\`www.${nakedDomain}\`) && PathPrefix(\`/\`)`,
|
||||||
service: `${id}`,
|
service: `${id}`,
|
||||||
tls: {
|
tls: isCustomSSL ? true : {
|
||||||
certresolver: 'letsencrypt'
|
certresolver: 'letsencrypt'
|
||||||
},
|
},
|
||||||
middlewares: []
|
middlewares: []
|
||||||
};
|
};
|
||||||
traefik.http.routers[`${id}-secure`] = {
|
traefik.http.routers[`${id}-secure`] = {
|
||||||
entrypoints: ['websecure'],
|
entrypoints: ['websecure'],
|
||||||
rule: `Host(\`${nakedDomain}\`)`,
|
rule: `Host(\`${nakedDomain}\`) && PathPrefix(\`/\`)`,
|
||||||
service: `${id}`,
|
service: `${id}`,
|
||||||
tls: {
|
tls: {
|
||||||
domains: {
|
domains: {
|
||||||
@@ -86,7 +86,7 @@ function configureMiddleware(
|
|||||||
} else {
|
} else {
|
||||||
traefik.http.routers[`${id}-secure-www`] = {
|
traefik.http.routers[`${id}-secure-www`] = {
|
||||||
entrypoints: ['websecure'],
|
entrypoints: ['websecure'],
|
||||||
rule: `Host(\`www.${nakedDomain}\`)`,
|
rule: `Host(\`www.${nakedDomain}\`) && PathPrefix(\`/\`)`,
|
||||||
service: `${id}`,
|
service: `${id}`,
|
||||||
tls: {
|
tls: {
|
||||||
domains: {
|
domains: {
|
||||||
@@ -97,9 +97,9 @@ function configureMiddleware(
|
|||||||
};
|
};
|
||||||
traefik.http.routers[`${id}-secure`] = {
|
traefik.http.routers[`${id}-secure`] = {
|
||||||
entrypoints: ['websecure'],
|
entrypoints: ['websecure'],
|
||||||
rule: `Host(\`${domain}\`)`,
|
rule: `Host(\`${domain}\`) && PathPrefix(\`/\`)`,
|
||||||
service: `${id}`,
|
service: `${id}`,
|
||||||
tls: {
|
tls: isCustomSSL ? true : {
|
||||||
certresolver: 'letsencrypt'
|
certresolver: 'letsencrypt'
|
||||||
},
|
},
|
||||||
middlewares: []
|
middlewares: []
|
||||||
@@ -110,14 +110,14 @@ function configureMiddleware(
|
|||||||
} else {
|
} else {
|
||||||
traefik.http.routers[id] = {
|
traefik.http.routers[id] = {
|
||||||
entrypoints: ['web'],
|
entrypoints: ['web'],
|
||||||
rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`,
|
rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/\`)`,
|
||||||
service: `${id}`,
|
service: `${id}`,
|
||||||
middlewares: []
|
middlewares: []
|
||||||
};
|
};
|
||||||
|
|
||||||
traefik.http.routers[`${id}-secure`] = {
|
traefik.http.routers[`${id}-secure`] = {
|
||||||
entrypoints: ['websecure'],
|
entrypoints: ['websecure'],
|
||||||
rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`,
|
rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/\`)`,
|
||||||
service: `${id}`,
|
service: `${id}`,
|
||||||
tls: {
|
tls: {
|
||||||
domains: {
|
domains: {
|
||||||
@@ -178,7 +178,19 @@ function configureMiddleware(
|
|||||||
|
|
||||||
export async function traefikConfiguration(request, reply) {
|
export async function traefikConfiguration(request, reply) {
|
||||||
try {
|
try {
|
||||||
|
const sslpath = '/etc/traefik/acme/custom';
|
||||||
|
const certificates = await prisma.certificate.findMany({ where: { team: { applications: { some: { settings: { isCustomSSL: true } } }, destinationDocker: { some: { remoteEngine: false, isCoolifyProxyUsed: true } } } } })
|
||||||
|
let parsedCertificates = []
|
||||||
|
for (const certificate of certificates) {
|
||||||
|
parsedCertificates.push({
|
||||||
|
certFile: `${sslpath}/${certificate.id}-cert.pem`,
|
||||||
|
keyFile: `${sslpath}/${certificate.id}-key.pem`
|
||||||
|
})
|
||||||
|
}
|
||||||
const traefik = {
|
const traefik = {
|
||||||
|
tls: {
|
||||||
|
certificates: parsedCertificates
|
||||||
|
},
|
||||||
http: {
|
http: {
|
||||||
routers: {},
|
routers: {},
|
||||||
services: {},
|
services: {},
|
||||||
@@ -224,7 +236,7 @@ export async function traefikConfiguration(request, reply) {
|
|||||||
port,
|
port,
|
||||||
destinationDocker,
|
destinationDocker,
|
||||||
destinationDockerId,
|
destinationDockerId,
|
||||||
settings: { previews, dualCerts }
|
settings: { previews, dualCerts, isCustomSSL }
|
||||||
} = application;
|
} = application;
|
||||||
if (destinationDockerId) {
|
if (destinationDockerId) {
|
||||||
const { network, id: dockerId } = destinationDocker;
|
const { network, id: dockerId } = destinationDocker;
|
||||||
@@ -244,7 +256,8 @@ export async function traefikConfiguration(request, reply) {
|
|||||||
isRunning,
|
isRunning,
|
||||||
isHttps,
|
isHttps,
|
||||||
isWWW,
|
isWWW,
|
||||||
isDualCerts: dualCerts
|
isDualCerts: dualCerts,
|
||||||
|
isCustomSSL
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (previews) {
|
if (previews) {
|
||||||
@@ -267,7 +280,8 @@ export async function traefikConfiguration(request, reply) {
|
|||||||
nakedDomain,
|
nakedDomain,
|
||||||
isHttps,
|
isHttps,
|
||||||
isWWW,
|
isWWW,
|
||||||
isDualCerts: dualCerts
|
isDualCerts: dualCerts,
|
||||||
|
isCustomSSL
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -534,7 +548,19 @@ export async function traefikOtherConfiguration(request: FastifyRequest<TraefikO
|
|||||||
export async function remoteTraefikConfiguration(request: FastifyRequest<OnlyId>) {
|
export async function remoteTraefikConfiguration(request: FastifyRequest<OnlyId>) {
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
try {
|
try {
|
||||||
|
const sslpath = '/etc/traefik/acme/custom';
|
||||||
|
const certificates = await prisma.certificate.findMany({ where: { team: { applications: { some: { settings: { isCustomSSL: true } } }, destinationDocker: { some: { id, remoteEngine: true, isCoolifyProxyUsed: true, remoteVerified: true } } } } })
|
||||||
|
let parsedCertificates = []
|
||||||
|
for (const certificate of certificates) {
|
||||||
|
parsedCertificates.push({
|
||||||
|
certFile: `${sslpath}/${certificate.id}-cert.pem`,
|
||||||
|
keyFile: `${sslpath}/${certificate.id}-key.pem`
|
||||||
|
})
|
||||||
|
}
|
||||||
const traefik = {
|
const traefik = {
|
||||||
|
tls: {
|
||||||
|
certificates: parsedCertificates
|
||||||
|
},
|
||||||
http: {
|
http: {
|
||||||
routers: {},
|
routers: {},
|
||||||
services: {},
|
services: {},
|
||||||
|
|||||||
@@ -1,38 +1,4 @@
|
|||||||
export interface OnlyId {
|
export interface OnlyId {
|
||||||
Params: { id: string },
|
Params: { id: string },
|
||||||
}
|
}
|
||||||
export interface SaveVersion extends OnlyId {
|
|
||||||
Body: {
|
|
||||||
version: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export interface SaveDatabaseDestination extends OnlyId {
|
|
||||||
Body: {
|
|
||||||
destinationId: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export interface GetDatabaseLogs extends OnlyId {
|
|
||||||
Querystring: {
|
|
||||||
since: number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export interface SaveDatabase extends OnlyId {
|
|
||||||
Body: {
|
|
||||||
name: string,
|
|
||||||
defaultDatabase: string,
|
|
||||||
dbUser: string,
|
|
||||||
dbUserPassword: string,
|
|
||||||
rootUser: string,
|
|
||||||
rootUserPassword: string,
|
|
||||||
version: string,
|
|
||||||
isRunning: boolean
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export interface SaveDatabaseSettings extends OnlyId {
|
|
||||||
Body: {
|
|
||||||
isPublic: boolean,
|
|
||||||
appendOnly: boolean
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -46,8 +46,10 @@
|
|||||||
"@tailwindcss/typography": "^0.5.7",
|
"@tailwindcss/typography": "^0.5.7",
|
||||||
"cuid": "2.1.8",
|
"cuid": "2.1.8",
|
||||||
"daisyui": "2.24.2",
|
"daisyui": "2.24.2",
|
||||||
|
"dayjs": "1.11.5",
|
||||||
"js-cookie": "3.0.1",
|
"js-cookie": "3.0.1",
|
||||||
"p-limit": "4.0.0",
|
"p-limit": "4.0.0",
|
||||||
|
"svelte-file-dropzone": "^1.0.0",
|
||||||
"svelte-select": "4.4.7",
|
"svelte-select": "4.4.7",
|
||||||
"sveltekit-i18n": "2.2.2"
|
"sveltekit-i18n": "2.2.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,33 +3,35 @@ import Cookies from 'js-cookie';
|
|||||||
|
|
||||||
export function getAPIUrl() {
|
export function getAPIUrl() {
|
||||||
if (GITPOD_WORKSPACE_URL) {
|
if (GITPOD_WORKSPACE_URL) {
|
||||||
const { href } = new URL(GITPOD_WORKSPACE_URL)
|
const { href } = new URL(GITPOD_WORKSPACE_URL);
|
||||||
const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, '')
|
const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, '');
|
||||||
return newURL
|
return newURL;
|
||||||
}
|
}
|
||||||
if (CODESANDBOX_HOST) {
|
if (CODESANDBOX_HOST) {
|
||||||
return `https://${CODESANDBOX_HOST.replace(/\$PORT/,'3001')}`
|
return `https://${CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`;
|
||||||
}
|
}
|
||||||
return dev ? 'http://localhost:3001' : 'http://localhost:3000';
|
return dev
|
||||||
|
? 'http://localhost:3001'
|
||||||
|
: 'http://localhost:3000';
|
||||||
}
|
}
|
||||||
export function getWebhookUrl(type: string) {
|
export function getWebhookUrl(type: string) {
|
||||||
if (GITPOD_WORKSPACE_URL) {
|
if (GITPOD_WORKSPACE_URL) {
|
||||||
const { href } = new URL(GITPOD_WORKSPACE_URL)
|
const { href } = new URL(GITPOD_WORKSPACE_URL);
|
||||||
const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, '')
|
const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, '');
|
||||||
if (type === 'github') {
|
if (type === 'github') {
|
||||||
return `${newURL}/webhooks/github/events`
|
return `${newURL}/webhooks/github/events`;
|
||||||
}
|
}
|
||||||
if (type === 'gitlab') {
|
if (type === 'gitlab') {
|
||||||
return `${newURL}/webhooks/gitlab/events`
|
return `${newURL}/webhooks/gitlab/events`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (CODESANDBOX_HOST) {
|
if (CODESANDBOX_HOST) {
|
||||||
const newURL = `https://${CODESANDBOX_HOST.replace(/\$PORT/,'3001')}`
|
const newURL = `https://${CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`;
|
||||||
if (type === 'github') {
|
if (type === 'github') {
|
||||||
return `${newURL}/webhooks/github/events`
|
return `${newURL}/webhooks/github/events`;
|
||||||
}
|
}
|
||||||
if (type === 'gitlab') {
|
if (type === 'gitlab') {
|
||||||
return `${newURL}/webhooks/gitlab/events`
|
return `${newURL}/webhooks/gitlab/events`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return `https://webhook.site/0e5beb2c-4e9b-40e2-a89e-32295e570c21/events`;
|
return `https://webhook.site/0e5beb2c-4e9b-40e2-a89e-32295e570c21/events`;
|
||||||
@@ -37,7 +39,7 @@ export function getWebhookUrl(type: string) {
|
|||||||
async function send({
|
async function send({
|
||||||
method,
|
method,
|
||||||
path,
|
path,
|
||||||
data = {},
|
data = null,
|
||||||
headers,
|
headers,
|
||||||
timeout = 120000
|
timeout = 120000
|
||||||
}: {
|
}: {
|
||||||
@@ -51,7 +53,7 @@ async function send({
|
|||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const id = setTimeout(() => controller.abort(), timeout);
|
const id = setTimeout(() => controller.abort(), timeout);
|
||||||
const opts: any = { method, headers: {}, body: null, signal: controller.signal };
|
const opts: any = { method, headers: {}, body: null, signal: controller.signal };
|
||||||
if (Object.keys(data).length > 0) {
|
if (data && Object.keys(data).length > 0) {
|
||||||
const parsedData = data;
|
const parsedData = data;
|
||||||
for (const [key, value] of Object.entries(data)) {
|
for (const [key, value] of Object.entries(data)) {
|
||||||
if (value === '') {
|
if (value === '') {
|
||||||
@@ -83,7 +85,9 @@ async function send({
|
|||||||
if (dev && !path.startsWith('https://')) {
|
if (dev && !path.startsWith('https://')) {
|
||||||
path = `${getAPIUrl()}${path}`;
|
path = `${getAPIUrl()}${path}`;
|
||||||
}
|
}
|
||||||
|
if (method === 'POST' && data && !opts.body) {
|
||||||
|
opts.body = data;
|
||||||
|
}
|
||||||
const response = await fetch(`${path}`, opts);
|
const response = await fetch(`${path}`, opts);
|
||||||
|
|
||||||
clearTimeout(id);
|
clearTimeout(id);
|
||||||
@@ -103,7 +107,11 @@ async function send({
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 401 && !path.startsWith('https://api.github') && !path.includes('/v4/user')) {
|
if (
|
||||||
|
response.status === 401 &&
|
||||||
|
!path.startsWith('https://api.github') &&
|
||||||
|
!path.includes('/v4/user')
|
||||||
|
) {
|
||||||
Cookies.remove('token');
|
Cookies.remove('token');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +134,7 @@ export function del(
|
|||||||
|
|
||||||
export function post(
|
export function post(
|
||||||
path: string,
|
path: string,
|
||||||
data: Record<string, unknown>,
|
data: Record<string, unknown> | FormData,
|
||||||
headers?: Record<string, unknown>
|
headers?: Record<string, unknown>
|
||||||
): Promise<Record<string, any>> {
|
): Promise<Record<string, any>> {
|
||||||
return send({ method: 'POST', path, data, headers });
|
return send({ method: 'POST', path, data, headers });
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { addToast } from '$lib/store';
|
|||||||
export const asyncSleep = (delay: number) =>
|
export const asyncSleep = (delay: number) =>
|
||||||
new Promise((resolve) => setTimeout(resolve, delay));
|
new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
|
|
||||||
export function errorNotification(error: any): void {
|
export function errorNotification(error: any | { message: string }): void {
|
||||||
if (error.message) {
|
if (error.message) {
|
||||||
if (error.message === 'Cannot read properties of undefined (reading \'postMessage\')') {
|
if (error.message === 'Cannot read properties of undefined (reading \'postMessage\')') {
|
||||||
return addToast({
|
return addToast({
|
||||||
@@ -83,4 +83,8 @@ export function handlerNotFoundLoad(error: any, url: URL) {
|
|||||||
status: 500,
|
status: 500,
|
||||||
error: new Error(`Could not load ${url}`)
|
error: new Error(`Could not load ${url}`)
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRndInteger(min: number, max: number) {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
}
|
}
|
||||||
@@ -13,8 +13,9 @@
|
|||||||
export let id: string;
|
export let id: string;
|
||||||
export let name: string;
|
export let name: string;
|
||||||
export let placeholder = '';
|
export let placeholder = '';
|
||||||
|
export let inputStyle = '';
|
||||||
|
|
||||||
let disabledClass = 'bg-coolback disabled:bg-coolblack';
|
let disabledClass = 'bg-coolback disabled:bg-coolblack w-full';
|
||||||
let isHttps = browser && window.location.protocol === 'https:';
|
let isHttps = browser && window.location.protocol === 'https:';
|
||||||
|
|
||||||
function copyToClipboard() {
|
function copyToClipboard() {
|
||||||
@@ -32,6 +33,7 @@
|
|||||||
{#if !isPasswordField || showPassword}
|
{#if !isPasswordField || showPassword}
|
||||||
{#if textarea}
|
{#if textarea}
|
||||||
<textarea
|
<textarea
|
||||||
|
style={inputStyle}
|
||||||
rows="5"
|
rows="5"
|
||||||
class={disabledClass}
|
class={disabledClass}
|
||||||
class:pr-10={true}
|
class:pr-10={true}
|
||||||
@@ -47,6 +49,7 @@
|
|||||||
>
|
>
|
||||||
{:else}
|
{:else}
|
||||||
<input
|
<input
|
||||||
|
style={inputStyle}
|
||||||
class={disabledClass}
|
class={disabledClass}
|
||||||
type="text"
|
type="text"
|
||||||
class:pr-10={true}
|
class:pr-10={true}
|
||||||
@@ -63,6 +66,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<input
|
<input
|
||||||
|
style={inputStyle}
|
||||||
class={disabledClass}
|
class={disabledClass}
|
||||||
class:pr-10={true}
|
class:pr-10={true}
|
||||||
class:pr-20={value && isHttps}
|
class:pr-20={value && isHttps}
|
||||||
@@ -78,7 +82,7 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="absolute top-0 right-0 m-3 cursor-pointer text-stone-600 hover:text-white">
|
<div class="absolute top-0 right-0 flex justify-center items-center h-full cursor-pointer text-stone-600 hover:text-white mr-3">
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
{#if isPasswordField}
|
{#if isPasswordField}
|
||||||
<div on:click={() => (showPassword = !showPassword)}>
|
<div on:click={() => (showPassword = !showPassword)}>
|
||||||
|
|||||||
@@ -10,23 +10,10 @@
|
|||||||
.slice(-16);
|
.slice(-16);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a {id} href={url} target="_blank" class="icons inline-block text-pink-500 cursor-pointer text-xs">
|
<a {id} href={url} target="_blank" class="icons inline-block cursor-pointer text-xs mx-2">
|
||||||
<svg
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" />
|
||||||
class="w-6 h-6"
|
</svg>
|
||||||
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="M6 4h11a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-11a1 1 0 0 1 -1 -1v-14a1 1 0 0 1 1 -1m3 0v18"
|
|
||||||
/>
|
|
||||||
<line x1="13" y1="8" x2="15" y2="8" />
|
|
||||||
<line x1="13" y1="12" x2="15" y2="12" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
</a>
|
||||||
<Tooltip triggeredBy={`#${id}`}>See details in the documentation</Tooltip>
|
<Tooltip triggeredBy={`#${id}`}>See details in the documentation</Tooltip>
|
||||||
|
|||||||
@@ -1,26 +1,38 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
// import { onMount } from 'svelte';
|
||||||
|
|
||||||
import Tooltip from './Tooltip.svelte';
|
// import Tooltip from './Tooltip.svelte';
|
||||||
export let explanation = '';
|
export let explanation = '';
|
||||||
let id: any;
|
export let position = 'dropdown-right'
|
||||||
let self: any;
|
// let id: any;
|
||||||
onMount(() => {
|
// let self: any;
|
||||||
id = `info-${self.offsetLeft}-${self.offsetTop}`;
|
// onMount(() => {
|
||||||
});
|
// id = `info-${self.offsetLeft}-${self.offsetTop}`;
|
||||||
|
// });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div {id} class="inline-block mx-2 text-pink-500 cursor-pointer" bind:this={self}>
|
<div class={`dropdown dropdown-end ${position}`}>
|
||||||
|
<label tabindex="0" class="btn btn-circle btn-ghost btn-xs text-sky-500">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-4 h-4 stroke-current"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||||
|
</label>
|
||||||
|
<div tabindex="0" class="card compact dropdown-content shadow bg-coolgray-400 rounded w-64">
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- <h2 class="card-title">You needed more info?</h2> -->
|
||||||
|
<p class="text-xs font-normal">{@html explanation}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- <div {id} class="inline-block mx-2 cursor-pointer" bind:this={self}>
|
||||||
<svg
|
<svg
|
||||||
fill="none"
|
fill="none"
|
||||||
height="18"
|
height="14"
|
||||||
shape-rendering="geometricPrecision"
|
shape-rendering="geometricPrecision"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
stroke-width="1.5"
|
stroke-width="1.4"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
width="18"
|
width="14"
|
||||||
><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z" /><path
|
><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z" /><path
|
||||||
d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3"
|
d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3"
|
||||||
/><circle cx="12" cy="17" r=".5" />
|
/><circle cx="12" cy="17" r=".5" />
|
||||||
@@ -28,4 +40,4 @@
|
|||||||
</div>
|
</div>
|
||||||
{#if id}
|
{#if id}
|
||||||
<Tooltip triggeredBy={`#${id}`}>{@html explanation}</Tooltip>
|
<Tooltip triggeredBy={`#${id}`}>{@html explanation}</Tooltip>
|
||||||
{/if}
|
{/if} -->
|
||||||
|
|||||||
@@ -15,9 +15,13 @@
|
|||||||
|
|
||||||
<div class="flex items-center py-4 pr-8">
|
<div class="flex items-center py-4 pr-8">
|
||||||
<div class="flex w-96 flex-col">
|
<div class="flex w-96 flex-col">
|
||||||
<div class="text-xs font-bold text-stone-100 md:text-base">
|
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||||
{title}<Explaner explanation={description} />
|
<label>
|
||||||
</div>
|
{title}
|
||||||
|
{#if description && description !== ''}
|
||||||
|
<Explaner explanation={description} />
|
||||||
|
{/if}
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class:text-center={isCenter} class="flex justify-center">
|
<div class:text-center={isCenter} class="flex justify-center">
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
export let type = 'info';
|
export let type = 'info';
|
||||||
|
function success() {
|
||||||
|
if (type === 'success') {
|
||||||
|
return 'bg-coollabs';
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -10,8 +15,7 @@
|
|||||||
on:focus={() => dispatch('pause')}
|
on:focus={() => dispatch('pause')}
|
||||||
on:mouseout={() => dispatch('resume')}
|
on:mouseout={() => dispatch('resume')}
|
||||||
on:blur={() => dispatch('resume')}
|
on:blur={() => dispatch('resume')}
|
||||||
class="alert shadow-lg text-white rounded hover:scale-105 transition-all duration-100 cursor-pointer"
|
class={`flex flex-row alert shadow-lg text-white hover:scale-105 transition-all duration-100 cursor-pointer rounded ${success()}`}
|
||||||
class:bg-coollabs={type === 'success'}
|
|
||||||
class:alert-error={type === 'error'}
|
class:alert-error={type === 'error'}
|
||||||
class:alert-info={type === 'info'}
|
class:alert-info={type === 'info'}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { fade } from 'svelte/transition';
|
|
||||||
import Toast from './Toast.svelte';
|
import Toast from './Toast.svelte';
|
||||||
|
|
||||||
import { dismissToast, pauseToast, resumeToast, toasts } from '$lib/store';
|
import { dismissToast, pauseToast, resumeToast, toasts } from '$lib/store';
|
||||||
@@ -7,9 +6,9 @@
|
|||||||
|
|
||||||
{#if $toasts}
|
{#if $toasts}
|
||||||
<section>
|
<section>
|
||||||
<article class="toast toast-top toast-end rounded-none" role="alert" transition:fade>
|
<article class="toast toast-top toast-end rounded-none px-10" role="alert" >
|
||||||
{#each $toasts as toast (toast.id)}
|
{#each $toasts as toast (toast.id)}
|
||||||
<Toast
|
<Toast
|
||||||
type={toast.type}
|
type={toast.type}
|
||||||
on:resume={() => resumeToast(toast.id)}
|
on:resume={() => resumeToast(toast.id)}
|
||||||
on:pause={() => pauseToast(toast.id)}
|
on:pause={() => pauseToast(toast.id)}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Tooltip } from 'flowbite-svelte';
|
import { Tooltip } from 'flowbite-svelte';
|
||||||
export let placement = 'bottom';
|
export let placement = 'bottom';
|
||||||
export let color = 'bg-coollabs text-left';
|
export let color = 'bg-coollabs font-thin text-left';
|
||||||
export let triggeredBy = '#tooltip-default';
|
export let triggeredBy = '#tooltip-default';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { dev } from '$app/env';
|
import { dev } from '$app/env';
|
||||||
import { get, post } from '$lib/api';
|
import { get, post } from '$lib/api';
|
||||||
import { addToast, appSession, features } from '$lib/store';
|
import { addToast, appSession, features, updateLoading, isUpdateAvailable } from '$lib/store';
|
||||||
import { asyncSleep, errorNotification } from '$lib/common';
|
import { asyncSleep, errorNotification } from '$lib/common';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import Tooltip from './Tooltip.svelte';
|
||||||
|
|
||||||
let isUpdateAvailable = false;
|
|
||||||
let updateStatus: any = {
|
let updateStatus: any = {
|
||||||
found: false,
|
found: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -57,37 +57,41 @@
|
|||||||
if ($appSession.userId) {
|
if ($appSession.userId) {
|
||||||
const overrideVersion = $features.latestVersion;
|
const overrideVersion = $features.latestVersion;
|
||||||
if ($appSession.teamId === '0') {
|
if ($appSession.teamId === '0') {
|
||||||
|
if ($updateLoading === true) return;
|
||||||
try {
|
try {
|
||||||
|
$updateLoading = true;
|
||||||
const data = await get(`/update`);
|
const data = await get(`/update`);
|
||||||
if (overrideVersion || data?.isUpdateAvailable) {
|
if (overrideVersion || data?.isUpdateAvailable) {
|
||||||
latestVersion = overrideVersion || data.latestVersion;
|
latestVersion = overrideVersion || data.latestVersion;
|
||||||
if (overrideVersion) {
|
if (overrideVersion) {
|
||||||
isUpdateAvailable = true;
|
$isUpdateAvailable = true;
|
||||||
} else {
|
} else {
|
||||||
isUpdateAvailable = data.isUpdateAvailable;
|
$isUpdateAvailable = data.isUpdateAvailable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorNotification(error);
|
return errorNotification(error);
|
||||||
|
} finally {
|
||||||
|
$updateLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col space-y-4 py-2">
|
<div class="py-0 lg:py-2">
|
||||||
{#if $appSession.teamId === '0'}
|
{#if $appSession.teamId === '0'}
|
||||||
{#if isUpdateAvailable}
|
{#if $isUpdateAvailable}
|
||||||
<button
|
<button
|
||||||
|
id="update"
|
||||||
disabled={updateStatus.success === false}
|
disabled={updateStatus.success === false}
|
||||||
on:click={update}
|
on:click={update}
|
||||||
class="icons tooltip tooltip-right tooltip-primary bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 text-white duration-75 hover:scale-105"
|
class="icons bg-coollabs-gradient text-white duration-75 hover:scale-105 w-full"
|
||||||
data-tip="Update Available!"
|
|
||||||
>
|
>
|
||||||
{#if updateStatus.loading}
|
{#if updateStatus.loading}
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="lds-heart h-9 w-8"
|
class="lds-heart h-8 w-8"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -101,24 +105,27 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
{:else if updateStatus.success === null}
|
{:else if updateStatus.success === null}
|
||||||
<svg
|
<div class="flex items-center justify-center space-x-2">
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<svg
|
||||||
class="h-9 w-8"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
class="h-8 w-8"
|
||||||
stroke-width="1.5"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke-width="1.5"
|
||||||
fill="none"
|
stroke="currentColor"
|
||||||
stroke-linecap="round"
|
fill="none"
|
||||||
stroke-linejoin="round"
|
stroke-linecap="round"
|
||||||
>
|
stroke-linejoin="round"
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
>
|
||||||
<circle cx="12" cy="12" r="9" />
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
<line x1="12" y1="8" x2="8" y2="12" />
|
<circle cx="12" cy="12" r="9" />
|
||||||
<line x1="12" y1="8" x2="12" y2="16" />
|
<line x1="12" y1="8" x2="8" y2="12" />
|
||||||
<line x1="16" y1="12" x2="12" y2="8" />
|
<line x1="12" y1="8" x2="12" y2="16" />
|
||||||
</svg>
|
<line x1="16" y1="12" x2="12" y2="8" />
|
||||||
|
</svg>
|
||||||
|
<span class="flex lg:hidden">Update available</span>
|
||||||
|
</div>
|
||||||
{:else if updateStatus.success}
|
{:else if updateStatus.success}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" class="h-9 w-8"
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" class="h-8 w-8"
|
||||||
><path
|
><path
|
||||||
fill="#DD2E44"
|
fill="#DD2E44"
|
||||||
d="M11.626 7.488c-.112.112-.197.247-.268.395l-.008-.008L.134 33.141l.011.011c-.208.403.14 1.223.853 1.937.713.713 1.533 1.061 1.936.853l.01.01L28.21 24.735l-.008-.009c.147-.07.282-.155.395-.269 1.562-1.562-.971-6.627-5.656-11.313-4.687-4.686-9.752-7.218-11.315-5.656z"
|
d="M11.626 7.488c-.112.112-.197.247-.268.395l-.008-.008L.134 33.141l.011.011c-.208.403.14 1.223.853 1.937.713.713 1.533 1.061 1.936.853l.01.01L28.21 24.735l-.008-.009c.147-.07.282-.155.395-.269 1.562-1.562-.971-6.627-5.656-11.313-4.687-4.686-9.752-7.218-11.315-5.656z"
|
||||||
@@ -183,6 +190,9 @@
|
|||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
<Tooltip triggeredBy="#update" placement="right" color="bg-coolgray-200 text-white"
|
||||||
|
>New Version Available!</Tooltip
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
20
apps/ui/src/lib/components/Upload.svelte
Normal file
20
apps/ui/src/lib/components/Upload.svelte
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { post } from '$lib/api';
|
||||||
|
let cert: any;
|
||||||
|
let key: any;
|
||||||
|
async function submitForm() {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('cert', cert[0]);
|
||||||
|
formData.append('key', key[0]);
|
||||||
|
await post('/upload', formData);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form on:submit|preventDefault={submitForm}>
|
||||||
|
<label for="cert">Certificate</label>
|
||||||
|
<input id="cert" type="file" required name="cert" bind:files={cert} />
|
||||||
|
<label for="key">Private Key</label>
|
||||||
|
<input id="key" type="file" required name="key" bind:files={key} />
|
||||||
|
<br />
|
||||||
|
<input type="submit" />
|
||||||
|
</form>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
export let server: any;
|
||||||
let usage = {
|
let usage = {
|
||||||
cpu: {
|
cpu: {
|
||||||
load: [0, 0, 0],
|
load: [0, 0, 0],
|
||||||
@@ -29,7 +30,7 @@
|
|||||||
async function getStatus() {
|
async function getStatus() {
|
||||||
if (loading.usage) return;
|
if (loading.usage) return;
|
||||||
loading.usage = true;
|
loading.usage = true;
|
||||||
const data = await get('/usage');
|
const data = await get(`/servers/usage/${server.id}?remoteEngine=${server.remoteEngine}`);
|
||||||
usage = data.usage;
|
usage = data.usage;
|
||||||
loading.usage = false;
|
loading.usage = false;
|
||||||
}
|
}
|
||||||
@@ -52,7 +53,7 @@
|
|||||||
async function manuallyCleanupStorage() {
|
async function manuallyCleanupStorage() {
|
||||||
try {
|
try {
|
||||||
loading.cleanup = true;
|
loading.cleanup = true;
|
||||||
await post('/internal/cleanup', {});
|
await post('/internal/cleanup', { serverId: server.id });
|
||||||
return addToast({
|
return addToast({
|
||||||
message: 'Cleanup done.',
|
message: 'Cleanup done.',
|
||||||
type: 'success'
|
type: 'success'
|
||||||
@@ -65,38 +66,63 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-full">
|
<div class="w-full relative p-5 ">
|
||||||
<div class="flex lg:flex-row flex-col gap-4">
|
{#if loading.usage}
|
||||||
<h1 class="title lg:text-3xl">Hardware Details</h1>
|
<span class="indicator-item badge bg-yellow-500 badge-sm" />
|
||||||
<div class="flex lg:flex-row flex-col space-x-0 lg:space-x-2 space-y-2 lg:space-y-0">
|
{:else}
|
||||||
{#if $appSession.teamId === '0'}
|
<span class="indicator-item badge bg-success badge-sm" />
|
||||||
<button on:click={manuallyCleanupStorage} class:loading={loading.cleanup} class="btn btn-sm"
|
{/if}
|
||||||
>Cleanup Storage</button
|
|
||||||
>
|
<div class="w-full flex flex-col lg:flex-row space-y-4 lg:space-y-0 space-x-4">
|
||||||
{/if}
|
<div class="flex flex-col">
|
||||||
|
<h1 class="font-bold text-lg lg:text-xl truncate">
|
||||||
|
{server.name}
|
||||||
|
{#if server.remoteEngine}
|
||||||
|
<span class="badge bg-coollabs-gradient rounded text-white"> BETA </span>
|
||||||
|
{/if}
|
||||||
|
</h1>
|
||||||
|
<div class="text-xs">
|
||||||
|
{#if server?.remoteIpAddress}
|
||||||
|
<h2>{server?.remoteIpAddress}</h2>
|
||||||
|
{:else}
|
||||||
|
<h2>localhost</h2>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{#if $appSession.teamId === '0'}
|
||||||
|
<button
|
||||||
|
disabled={loading.cleanup}
|
||||||
|
on:click={manuallyCleanupStorage}
|
||||||
|
class:loading={loading.cleanup}
|
||||||
|
class:bg-coollabs={!loading.cleanup}
|
||||||
|
class="btn btn-sm">Cleanup Storage</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="flex lg:flex-row flex-col gap-4">
|
||||||
|
<div class="flex lg:flex-row flex-col space-x-0 lg:space-x-2 space-y-2 lg:space-y-0" />
|
||||||
</div>
|
</div>
|
||||||
<div class="divider" />
|
<div class="divider" />
|
||||||
<div class="grid grid-flow-col gap-4 grid-rows-3 justify-start lg:justify-center lg:grid-rows-1">
|
<div class="grid grid-flow-col gap-4 grid-rows-3 justify-start lg:justify-center lg:grid-rows-1">
|
||||||
<div class="stats stats-vertical min-w-[16rem] mb-5 rounded bg-transparent">
|
<div class="stats stats-vertical min-w-[16rem] mb-5 rounded bg-transparent">
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-title">Total Memory</div>
|
<div class="stat-title">Total Memory</div>
|
||||||
<div class="stat-value text-2xl">
|
<div class="stat-value text-2xl text-white">
|
||||||
{(usage?.memory.totalMemMb).toFixed(0)}<span class="text-sm">MB</span>
|
{(usage?.memory?.totalMemMb).toFixed(0)}<span class="text-sm">MB</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-title">Used Memory</div>
|
<div class="stat-title">Used Memory</div>
|
||||||
<div class="stat-value text-2xl">
|
<div class="stat-value text-2xl text-white">
|
||||||
{(usage?.memory.usedMemMb).toFixed(0)}<span class="text-sm">MB</span>
|
{(usage?.memory?.usedMemMb).toFixed(0)}<span class="text-sm">MB</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-title">Free Memory</div>
|
<div class="stat-title">Free Memory</div>
|
||||||
<div class="stat-value text-2xl">
|
<div class="stat-value text-2xl text-white">
|
||||||
{usage?.memory.freeMemPercentage}<span class="text-sm">%</span>
|
{(usage?.memory?.freeMemPercentage).toFixed(0)}<span class="text-sm">%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -104,42 +130,42 @@
|
|||||||
<div class="stats stats-vertical min-w-[20rem] mb-5 bg-transparent rounded">
|
<div class="stats stats-vertical min-w-[20rem] mb-5 bg-transparent rounded">
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-title">Total CPU</div>
|
<div class="stat-title">Total CPU</div>
|
||||||
<div class="stat-value text-2xl">
|
<div class="stat-value text-2xl text-white">
|
||||||
{usage?.cpu.count}
|
{usage?.cpu?.count}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-title">CPU Usage</div>
|
<div class="stat-title">CPU Usage</div>
|
||||||
<div class="stat-value text-2xl">
|
<div class="stat-value text-2xl text-white">
|
||||||
{usage?.cpu.usage}<span class="text-sm">%</span>
|
{usage?.cpu?.usage}<span class="text-sm">%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-title">Load Average (5,10,30mins)</div>
|
<div class="stat-title">Load Average (5,10,30mins)</div>
|
||||||
<div class="stat-value text-2xl">{usage?.cpu.load}</div>
|
<div class="stat-value text-2xl text-white">{usage?.cpu?.load}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stats stats-vertical min-w-[16rem] mb-5 bg-transparent rounded">
|
<div class="stats stats-vertical min-w-[16rem] mb-5 bg-transparent rounded">
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-title">Total Disk</div>
|
<div class="stat-title">Total Disk</div>
|
||||||
<div class="stat-value text-2xl">
|
<div class="stat-value text-2xl text-white">
|
||||||
{usage?.disk.totalGb}<span class="text-sm">GB</span>
|
{usage?.disk?.totalGb}<span class="text-sm">GB</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-title">Used Disk</div>
|
<div class="stat-title">Used Disk</div>
|
||||||
<div class="stat-value text-2xl">
|
<div class="stat-value text-2xl text-white">
|
||||||
{usage?.disk.usedGb}<span class="text-sm">GB</span>
|
{usage?.disk?.usedGb}<span class="text-sm">GB</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-title">Free Disk</div>
|
<div class="stat-title">Free Disk</div>
|
||||||
<div class="stat-value text-2xl">
|
<div class="stat-value text-2xl text-white">
|
||||||
{usage?.disk.freePercentage}<span class="text-sm">%</span>
|
{usage?.disk?.freePercentage}<span class="text-sm">%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,4 +16,6 @@
|
|||||||
<Icons.Redis {isAbsolute} />
|
<Icons.Redis {isAbsolute} />
|
||||||
{:else if type === 'couchdb'}
|
{:else if type === 'couchdb'}
|
||||||
<Icons.CouchDB {isAbsolute} />
|
<Icons.CouchDB {isAbsolute} />
|
||||||
|
{:else if type === 'edgedb'}
|
||||||
|
<Icons.EdgeDB {isAbsolute} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
22
apps/ui/src/lib/components/svg/databases/EdgeDB.svelte
Normal file
22
apps/ui/src/lib/components/svg/databases/EdgeDB.svelte
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let isAbsolute = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
class={isAbsolute ? 'absolute top-0 left-0 -m-8 h-16 w-16' : 'mx-auto w-12 h-12'}
|
||||||
|
width="88"
|
||||||
|
fill="#1F8AED"
|
||||||
|
height="101"
|
||||||
|
viewBox="0 -15 88 101"
|
||||||
|
><path
|
||||||
|
class="pageNav_logoBar__2v4ah"
|
||||||
|
style="transform-origin:center 35.5px"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M55.1436 71H58.1436V0H55.1436V71Z"
|
||||||
|
/><path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M74.5362 35.3047C74.5362 41.3776 72.1013 42.4662 69.3799 42.4662H63.5935V28.1432H69.3799C72.1013 28.1432 74.5362 29.2318 74.5362 35.3047V35.3047ZM71.5862 35.3047C71.5862 31.0651 70.2971 30.8646 68.4352 30.8646H66.6305V39.7448H68.4352C70.2971 39.7448 71.5862 39.5443 71.5862 35.3047V35.3047ZM40.9348 42.4662V28.1432H50.0442V30.8646H43.9713V33.7865H48.5546V36.4792H43.9713V39.7448H50.0442V42.4662H40.9348ZM80.6092 36.1068V39.7448H83.13C84.7055 39.7448 85.1066 38.7135 85.1066 37.9401C85.1066 37.3385 84.8201 36.1068 82.6717 36.1068H80.6092ZM80.6092 30.8646V33.5859H82.6717C83.8462 33.5859 84.5337 33.0703 84.5337 32.2109C84.5337 31.3516 83.8462 30.8646 82.6717 30.8646H80.6092ZM77.5732 28.1432H83.4169C86.482 28.1432 87.3987 30.2917 87.3987 31.8385C87.3987 33.2708 86.482 34.3021 85.8518 34.5885C87.6851 35.4766 88.0002 37.2813 88.0002 38.1979C88.0002 39.401 87.3987 42.4662 83.4169 42.4662H77.5732V28.1432ZM23.4899 35.3047C23.4899 41.3776 21.055 42.4662 18.3337 42.4662H12.5472V28.1432H18.3337C21.055 28.1432 23.4899 29.2318 23.4899 35.3047V35.3047ZM32.4272 39.8594C33.974 39.8594 34.7761 39.3438 35.0626 39V37.4245H32.599V34.9609H37.4975V40.6615C37.0678 41.3203 34.7188 42.6094 32.5704 42.6094C29.047 42.6094 26.0678 41.2344 26.0678 35.1615C26.0678 29.0885 29.0756 28 31.797 28C36.0652 28 37.1251 30.2344 37.4688 32.2109L34.948 32.7839C34.8048 31.8672 34.0027 30.7214 32.1694 30.7214C30.3074 30.7214 29.0183 30.9219 29.0183 35.1615C29.0183 39.401 30.3647 39.8594 32.4272 39.8594V39.8594ZM20.539 35.3047C20.539 31.0651 19.2499 30.8646 17.3879 30.8646H15.5833V39.7448H17.3879C19.2499 39.7448 20.539 39.5443 20.539 35.3047V35.3047ZM0 42.4662V28.1432H9.10938V30.8646H3.03646V33.7865H7.61979V36.4792H3.03646V39.7448H9.10938V42.4662H0Z"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
@@ -9,15 +9,8 @@
|
|||||||
viewBox="0 0 309.88 252.72"
|
viewBox="0 0 309.88 252.72"
|
||||||
class={isAbsolute ? 'absolute top-0 left-0 -m-5 h-12 w-12 ' : 'mx-auto w-8 h-8'}
|
class={isAbsolute ? 'absolute top-0 left-0 -m-5 h-12 w-12 ' : 'mx-auto w-8 h-8'}
|
||||||
>
|
>
|
||||||
<defs>
|
|
||||||
<style>
|
|
||||||
.cls-1 {
|
|
||||||
fill: #fff;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</defs>
|
|
||||||
<path
|
<path
|
||||||
class="cls-1"
|
fill="#fff"
|
||||||
d="M316,10.05a4.2,4.2,0,0,0-2.84-1c-2.84,0-6.5,1.92-8.46,3l-.79.4a26.81,26.81,0,0,1-10.57,2.66c-3.76.12-7,.34-11.22.77-25,2.58-36.15,21.74-46.89,40.27-5.84,10.08-11.88,20.5-20.16,28.57a55.71,55.71,0,0,1-5.46,4.63c-8.57,6.39-19.33,10.9-27.74,14.12-8.07,3.08-16.86,5.85-25.37,8.53-7.78,2.45-15.14,4.76-21.9,7.28-3.05,1.13-5.64,2-7.93,2.76-6.15,2-10.6,3.53-17.08,8-2.53,1.73-5.07,3.6-6.8,5a71.26,71.26,0,0,0-13.54,14.27A84.81,84.81,0,0,1,77.88,163c-1.36,1.34-3.8,2-7.43,2-4.27,0-9.43-.88-14.91-1.81s-11.46-2-16.46-2c-4.07,0-7.17.66-9.5,2,0,0-3.9,2.28-5.56,5.23l1.62.73a33.56,33.56,0,0,1,6.93,5,33.68,33.68,0,0,0,7.19,5.12A6.37,6.37,0,0,1,42,180.72c-.69,1-1.69,2.29-2.74,3.67-5.77,7.55-9.13,12.32-7.2,14.92a6,6,0,0,0,3,.68c12.59,0,19.34-3.27,27.9-7.41,2.47-1.2,5-2.44,8-3.7,5-2.17,10.38-5.63,16.08-9.29,7.55-4.85,15.36-9.87,22.92-12.3a62.3,62.3,0,0,1,19.23-2.7c8,0,16.42,1.07,24.54,2.11,6.06.78,12.32,1.58,18.47,2,2.39.14,4.6.21,6.76.21a78.48,78.48,0,0,0,8.61-.45l.68-.24c4.32-2.65,6.34-8.34,8.29-13.84,1.26-3.54,2.32-6.72,4-8.74a2.06,2.06,0,0,1,.33-.27.4.4,0,0,1,.49.08.25.25,0,0,1,0,.16c-1,21.51-9.67,35.16-18.42,47.3L177,199.14s8.18,0,12.84-1.8c17-5.08,29.84-16.28,39.18-34.14a144.39,144.39,0,0,0,6.16-14.09c.16-.4,1.64-1.14,1.49.93,0,.61-.08,1.29-.13,2h0c0,.42-.06.85-.08,1.28-.25,3-1,9.34-1,9.34l5.25-2.81c12.66-8,22.42-24.14,29.82-49.25,3.09-10.46,5.34-20.85,7.33-30,2.38-11,4.43-20.43,6.78-24.09,3.69-5.74,9.32-9.62,14.77-13.39.75-.51,1.49-1,2.22-1.54,6.86-4.81,13.67-10.36,15.16-20.71l0-.23C317.93,12.92,317,11,316,10.05Z"
|
d="M316,10.05a4.2,4.2,0,0,0-2.84-1c-2.84,0-6.5,1.92-8.46,3l-.79.4a26.81,26.81,0,0,1-10.57,2.66c-3.76.12-7,.34-11.22.77-25,2.58-36.15,21.74-46.89,40.27-5.84,10.08-11.88,20.5-20.16,28.57a55.71,55.71,0,0,1-5.46,4.63c-8.57,6.39-19.33,10.9-27.74,14.12-8.07,3.08-16.86,5.85-25.37,8.53-7.78,2.45-15.14,4.76-21.9,7.28-3.05,1.13-5.64,2-7.93,2.76-6.15,2-10.6,3.53-17.08,8-2.53,1.73-5.07,3.6-6.8,5a71.26,71.26,0,0,0-13.54,14.27A84.81,84.81,0,0,1,77.88,163c-1.36,1.34-3.8,2-7.43,2-4.27,0-9.43-.88-14.91-1.81s-11.46-2-16.46-2c-4.07,0-7.17.66-9.5,2,0,0-3.9,2.28-5.56,5.23l1.62.73a33.56,33.56,0,0,1,6.93,5,33.68,33.68,0,0,0,7.19,5.12A6.37,6.37,0,0,1,42,180.72c-.69,1-1.69,2.29-2.74,3.67-5.77,7.55-9.13,12.32-7.2,14.92a6,6,0,0,0,3,.68c12.59,0,19.34-3.27,27.9-7.41,2.47-1.2,5-2.44,8-3.7,5-2.17,10.38-5.63,16.08-9.29,7.55-4.85,15.36-9.87,22.92-12.3a62.3,62.3,0,0,1,19.23-2.7c8,0,16.42,1.07,24.54,2.11,6.06.78,12.32,1.58,18.47,2,2.39.14,4.6.21,6.76.21a78.48,78.48,0,0,0,8.61-.45l.68-.24c4.32-2.65,6.34-8.34,8.29-13.84,1.26-3.54,2.32-6.72,4-8.74a2.06,2.06,0,0,1,.33-.27.4.4,0,0,1,.49.08.25.25,0,0,1,0,.16c-1,21.51-9.67,35.16-18.42,47.3L177,199.14s8.18,0,12.84-1.8c17-5.08,29.84-16.28,39.18-34.14a144.39,144.39,0,0,0,6.16-14.09c.16-.4,1.64-1.14,1.49.93,0,.61-.08,1.29-.13,2h0c0,.42-.06.85-.08,1.28-.25,3-1,9.34-1,9.34l5.25-2.81c12.66-8,22.42-24.14,29.82-49.25,3.09-10.46,5.34-20.85,7.33-30,2.38-11,4.43-20.43,6.78-24.09,3.69-5.74,9.32-9.62,14.77-13.39.75-.51,1.49-1,2.22-1.54,6.86-4.81,13.67-10.36,15.16-20.71l0-.23C317.93,12.92,317,11,316,10.05Z"
|
||||||
transform="translate(-7.45 -9.1)"
|
transform="translate(-7.45 -9.1)"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ export { default as MongoDB } from './MongoDB.svelte';
|
|||||||
export { default as MySQL } from './MySQL.svelte';
|
export { default as MySQL } from './MySQL.svelte';
|
||||||
export { default as PostgreSQL } from './PostgreSQL.svelte';
|
export { default as PostgreSQL } from './PostgreSQL.svelte';
|
||||||
export { default as Redis } from './Redis.svelte';
|
export { default as Redis } from './Redis.svelte';
|
||||||
|
export { default as EdgeDB } from './EdgeDB.svelte';
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<svg
|
<svg
|
||||||
viewBox="0 0 700 240"
|
viewBox="0 0 700 240"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class={isAbsolute ? 'w-36 absolute top-0 left-0 -m-3 -mt-5' : 'w-28 h-28 mx-auto'}
|
class={isAbsolute ? 'w-36 absolute top-0 left-0 -m-3 -mt-5' : 'w-full h-10 mx-auto'}
|
||||||
><path fill="#FDBC3D" d="m90.694 107.498-.981.39-20.608 8.23 6.332 6.547z" /><path
|
><path fill="#FDBC3D" d="m90.694 107.498-.981.39-20.608 8.23 6.332 6.547z" /><path
|
||||||
fill="#8EC63F"
|
fill="#8EC63F"
|
||||||
d="M61.139 77.914 46.632 93 56.9 103.547c8.649-7.169 17.832-10.502 18.653-10.789L61.139 77.914z"
|
d="M61.139 77.914 46.632 93 56.9 103.547c8.649-7.169 17.832-10.502 18.653-10.789L61.139 77.914z"
|
||||||
|
|||||||
9
apps/ui/src/lib/components/svg/services/Grafana.svelte
Normal file
9
apps/ui/src/lib/components/svg/services/Grafana.svelte
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let isAbsolute = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<img
|
||||||
|
alt="grafana logo"
|
||||||
|
class={isAbsolute ? 'w-12 h-12 absolute top-0 left-0 -m-3 -mt-5' : 'w-8 h-8 mx-auto'}
|
||||||
|
src="/grafana.png"
|
||||||
|
/>
|
||||||
@@ -42,4 +42,8 @@
|
|||||||
<Icons.Searxng {isAbsolute} />
|
<Icons.Searxng {isAbsolute} />
|
||||||
{:else if type === 'weblate'}
|
{:else if type === 'weblate'}
|
||||||
<Icons.Weblate {isAbsolute} />
|
<Icons.Weblate {isAbsolute} />
|
||||||
|
{:else if type === 'grafana'}
|
||||||
|
<Icons.Grafana {isAbsolute} />
|
||||||
|
{:else if type === 'trilium'}
|
||||||
|
<Icons.Trilium {isAbsolute} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
9
apps/ui/src/lib/components/svg/services/Trilium.svelte
Normal file
9
apps/ui/src/lib/components/svg/services/Trilium.svelte
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let isAbsolute = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<img
|
||||||
|
alt="trilium logo"
|
||||||
|
class={isAbsolute ? 'w-9 h-9 absolute top-3 left-0 -m-3 -mt-5' : 'w-8 h-8 mx-auto'}
|
||||||
|
src="/trilium.png"
|
||||||
|
/>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class={isAbsolute ? 'w-16 h-16 absolute top-0 left-0 -m-7' : 'w-12 h-12 mx-auto'}
|
class={isAbsolute ? 'w-16 h-16 absolute top-0 left-0 -m-7' : 'w-10 h-10 mx-auto'}
|
||||||
version="1.1"
|
version="1.1"
|
||||||
viewBox="0 0 300 300"
|
viewBox="0 0 300 300"
|
||||||
><linearGradient
|
><linearGradient
|
||||||
|
|||||||
@@ -17,4 +17,6 @@ export { default as Appwrite } from './Appwrite.svelte';
|
|||||||
export { default as Moodle } from './Moodle.svelte';
|
export { default as Moodle } from './Moodle.svelte';
|
||||||
export { default as GlitchTip } from './GlitchTip.svelte';
|
export { default as GlitchTip } from './GlitchTip.svelte';
|
||||||
export { default as Searxng } from './Searxng.svelte';
|
export { default as Searxng } from './Searxng.svelte';
|
||||||
export { default as Weblate } from './Weblate.svelte';
|
export { default as Weblate } from './Weblate.svelte';
|
||||||
|
export { default as Grafana } from './Grafana.svelte';
|
||||||
|
export { default as Trilium } from './Trilium.svelte'
|
||||||
|
|||||||
7
apps/ui/src/lib/dayjs.ts
Normal file
7
apps/ui/src/lib/dayjs.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
import utc from 'dayjs/plugin/utc.js';
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime.js';
|
||||||
|
dayjs.extend(utc);
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
|
export { dayjs as day };
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
"removing": "Removing...",
|
"removing": "Removing...",
|
||||||
"remove_domain": "Remove domain",
|
"remove_domain": "Remove domain",
|
||||||
"public_port_range": "Public Port Range",
|
"public_port_range": "Public Port Range",
|
||||||
"public_port_range_explainer": "Ports used to expose databases/services/internal services.<br> Add them to your firewall (if applicable).<br><br>You can specify a range of ports, eg: <span class='text-settings font-bold'>9000-9100</span>",
|
"public_port_range_explainer": "Ports used to expose databases/services/internal services.<br> Add them to your firewall (if applicable).<br><br>You can specify a range of ports, eg: <span class='text-settings '>9000-9100</span>",
|
||||||
"no_actions_available": "No actions available",
|
"no_actions_available": "No actions available",
|
||||||
"admin_api_key": "Admin API key"
|
"admin_api_key": "Admin API key"
|
||||||
},
|
},
|
||||||
@@ -144,8 +144,8 @@
|
|||||||
},
|
},
|
||||||
"preview": {
|
"preview": {
|
||||||
"need_during_buildtime": "Need during buildtime?",
|
"need_during_buildtime": "Need during buildtime?",
|
||||||
"setup_secret_app_first": "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments.",
|
"setup_secret_app_first": "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>Useful for creating <span class='text-settings '>staging</span> environments.",
|
||||||
"values_overwriting_app_secrets": "These values overwrite application secrets in PR/MR deployments. Useful for creating <span class='text-green-500 font-bold'>staging</span> environments.",
|
"values_overwriting_app_secrets": "These values overwrite application secrets in PR/MR deployments. Useful for creating <span class='text-settings '>staging</span> environments.",
|
||||||
"redeploy": "Redeploy",
|
"redeploy": "Redeploy",
|
||||||
"no_previews_available": "No previews available"
|
"no_previews_available": "No previews available"
|
||||||
},
|
},
|
||||||
@@ -159,7 +159,7 @@
|
|||||||
"storage_saved": "Storage saved.",
|
"storage_saved": "Storage saved.",
|
||||||
"storage_updated": "Storage updated.",
|
"storage_updated": "Storage updated.",
|
||||||
"storage_deleted": "Storage deleted.",
|
"storage_deleted": "Storage deleted.",
|
||||||
"persistent_storage_explainer": "You can specify any folder that you want to be persistent across deployments.<br><span class='text-green-500 font-bold'>/example</span> means it will preserve <span class='text-green-500 font-bold'>/app/example</span> in the container as <span class='text-green-500 font-bold'>/app</span> is <span class='text-green-500 font-bold'>the root directory</span> for your application.<br><br>This is useful for storing data such as a <span class='text-green-500 font-bold'>database (SQLite)</span> or a <span class='text-green-500 font-bold'>cache</span>."
|
"persistent_storage_explainer": "You can specify any folder that you want to be persistent across deployments.<br><span class='text-settings '>/example</span> means it will preserve <span class='text-settings '>/app/example</span> in the container as <span class='text-settings '>/app</span> is <span class='text-settings '>the root directory</span> for your application.<br><br>This is useful for storing data such as a <span class='text-settings '>database (SQLite)</span> or a <span class='text-settings '>cache</span>."
|
||||||
},
|
},
|
||||||
"deployment_queued": "Deployment queued.",
|
"deployment_queued": "Deployment queued.",
|
||||||
"confirm_to_delete": "Are you sure you would like to delete '{{name}}'?",
|
"confirm_to_delete": "Are you sure you would like to delete '{{name}}'?",
|
||||||
@@ -194,14 +194,14 @@
|
|||||||
"application": "Application",
|
"application": "Application",
|
||||||
"url_fqdn": "URL (FQDN)",
|
"url_fqdn": "URL (FQDN)",
|
||||||
"domain_fqdn": "Domain (FQDN)",
|
"domain_fqdn": "Domain (FQDN)",
|
||||||
"https_explainer": "If you specify <span class='text-green-500 font-bold'>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-green-500 font-bold'>www</span>, the application will be redirected (302) from non-www and vice versa.<br><br>To modify the domain, you must first stop the application.<br><br><span class='text-white font-bold'>You must set your DNS to point to the server IP in advance.</span>",
|
"https_explainer": "If you specify <span class='text-settings '>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-settings '>www</span>, the application will be redirected (302) from non-www and vice versa.<br><br>To modify the domain, you must first stop the application.<br><br><span class='text-white '>You must set your DNS to point to the server IP in advance.</span>",
|
||||||
"ssl_www_and_non_www": "Generate SSL for www and non-www?",
|
"ssl_www_and_non_www": "Generate SSL for www and non-www?",
|
||||||
"ssl_explainer": "It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-green-500'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both.",
|
"ssl_explainer": "It will generate certificates for both www and non-www. <br>You need to have <span class=' text-settings'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both.",
|
||||||
"install_command": "Install Command",
|
"install_command": "Install Command",
|
||||||
"build_command": "Build Command",
|
"build_command": "Build Command",
|
||||||
"start_command": "Start Command",
|
"start_command": "Start Command",
|
||||||
"directory_to_use_explainer": "Directory to use as the base for all commands.<br>Could be useful with <span class='text-green-500 font-bold'>monorepos</span>.",
|
"directory_to_use_explainer": "Directory to use as the base for all commands.<br>Could be useful with <span class='text-settings '>monorepos</span>.",
|
||||||
"publish_directory_explainer": "Directory containing all the assets for deployment. <br> For example: <span class='text-green-500 font-bold'>dist</span>,<span class='text-green-500 font-bold'>_site</span> or <span class='text-green-500 font-bold'>public</span>.",
|
"publish_directory_explainer": "Directory containing all the assets for deployment. <br> For example: <span class='text-settings '>dist</span>,<span class='text-settings '>_site</span> or <span class='text-settings '>public</span>.",
|
||||||
"features": "Features",
|
"features": "Features",
|
||||||
"enable_automatic_deployment": "Enable Automatic Deployment",
|
"enable_automatic_deployment": "Enable Automatic Deployment",
|
||||||
"enable_auto_deploy_webhooks": "Enable automatic deployment through webhooks.",
|
"enable_auto_deploy_webhooks": "Enable automatic deployment through webhooks.",
|
||||||
@@ -209,7 +209,7 @@
|
|||||||
"expose_a_port": "Expose a port",
|
"expose_a_port": "Expose a port",
|
||||||
"enable_preview_deploy_mr_pr_requests": "Enable preview deployments from pull or merge requests.",
|
"enable_preview_deploy_mr_pr_requests": "Enable preview deployments from pull or merge requests.",
|
||||||
"debug_logs": "Debug Logs",
|
"debug_logs": "Debug Logs",
|
||||||
"enable_debug_log_during_build": "Enable debug logs during build phase.<br><span class='text-settings font-bold'>Sensitive information</span> could be visible and saved in logs.",
|
"enable_debug_log_during_build": "Enable debug logs during build phase.<br><span class='text-settings '>Sensitive information</span> could be visible and saved in logs.",
|
||||||
"cant_activate_auto_deploy_without_repo": "Cannot activate automatic deployments until only one application is defined for this repository / branch.",
|
"cant_activate_auto_deploy_without_repo": "Cannot activate automatic deployments until only one application is defined for this repository / branch.",
|
||||||
"no_applications_found": "No applications found",
|
"no_applications_found": "No applications found",
|
||||||
"secret__batch_dot_env": "Paste .env file",
|
"secret__batch_dot_env": "Paste .env file",
|
||||||
@@ -223,7 +223,7 @@
|
|||||||
"set_public": "Set it public",
|
"set_public": "Set it public",
|
||||||
"warning_database_public": "Your database will be reachable over the internet. <br>Take security seriously in this case!",
|
"warning_database_public": "Your database will be reachable over the internet. <br>Take security seriously in this case!",
|
||||||
"change_append_only_mode": "Change append only mode",
|
"change_append_only_mode": "Change append only mode",
|
||||||
"warning_append_only": "Useful if you would like to restore redis data from a backup.<br><span class='font-bold text-white'>Database restart is required.</span>",
|
"warning_append_only": "Useful if you would like to restore redis data from a backup.<br><span class=' text-white'>Database restart is required.</span>",
|
||||||
"select_database_type": "Select a Database type",
|
"select_database_type": "Select a Database type",
|
||||||
"select_database_version": "Select a Database version",
|
"select_database_version": "Select a Database version",
|
||||||
"confirm_stop": "Are you sure you would like to stop {{name}}?",
|
"confirm_stop": "Are you sure you would like to stop {{name}}?",
|
||||||
@@ -275,7 +275,7 @@
|
|||||||
"application_id": "Application ID",
|
"application_id": "Application ID",
|
||||||
"group_name": "Group Name",
|
"group_name": "Group Name",
|
||||||
"oauth_id": "OAuth ID",
|
"oauth_id": "OAuth ID",
|
||||||
"oauth_id_explainer": "The OAuth ID is the unique identifier of the GitLab application. <br>You can find it <span class='font-bold text-settings' >in the URL</span> of your GitLab OAuth Application.",
|
"oauth_id_explainer": "The OAuth ID is the unique identifier of the GitLab application. <br>You can find it <span class=' text-settings' >in the URL</span> of your GitLab OAuth Application.",
|
||||||
"register_oauth_gitlab": "Register new OAuth application on GitLab",
|
"register_oauth_gitlab": "Register new OAuth application on GitLab",
|
||||||
"gitlab": {
|
"gitlab": {
|
||||||
"self_hosted": "Instance-wide application (self-hosted)",
|
"self_hosted": "Instance-wide application (self-hosted)",
|
||||||
@@ -290,7 +290,7 @@
|
|||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"all_email_verified": "All emails are verified. You can login now.",
|
"all_email_verified": "All emails are verified. You can login now.",
|
||||||
"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-pink-600'>both DNS entries</span> set in advance.<br><br>Service needs to be restarted."
|
"generate_www_non_www_ssl": "It will generate certificates for both www and non-www. <br>You need to have <span class='text-settings'>both DNS entries</span> set in advance.<br><br>Service needs to be restarted."
|
||||||
},
|
},
|
||||||
"service": {
|
"service": {
|
||||||
"stop_service": "Stop",
|
"stop_service": "Stop",
|
||||||
@@ -306,15 +306,15 @@
|
|||||||
"change_language": "Change Language",
|
"change_language": "Change Language",
|
||||||
"permission_denied": "You do not have permission to do this. \\nAsk an admin to modify your permissions.",
|
"permission_denied": "You do not have permission to do this. \\nAsk an admin to modify your permissions.",
|
||||||
"domain_removed": "Domain removed",
|
"domain_removed": "Domain removed",
|
||||||
"ssl_explainer": "If you specify <span class='text-settings 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-settings font-bold'>www</span>, Coolify will be redirected (302) from non-www and vice versa.<br><br><span class='text-settings font-bold'>WARNING:</span> If you change an already set domain, it will break webhooks and other integrations! You need to manually update them.",
|
"ssl_explainer": "If you specify <span class='text-settings'>https</span>, Coolify will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-settings '>www</span>, Coolify will be redirected (302) from non-www and vice versa.<br><br><span class='text-settings '>WARNING:</span> If you change an already set domain, it will break 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.",
|
"must_remove_domain_before_changing": "Must remove the domain before you can change this setting.",
|
||||||
"registration_allowed": "Registration allowed?",
|
"registration_allowed": "Registration allowed?",
|
||||||
"registration_allowed_explainer": "Allow further registrations to the application. <br>It's turned off after the first registration.",
|
"registration_allowed_explainer": "Allow further registrations to the application. <br>It's turned off after the first registration.",
|
||||||
"coolify_proxy_settings": "Coolify Proxy Settings",
|
"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 \" href=\"{{link}}\" target=\"_blank\">stats</a> page.",
|
||||||
"auto_update_enabled": "Auto update enabled?",
|
"auto_update_enabled": "Auto update enabled?",
|
||||||
"auto_update_enabled_explainer": "Enable automatic updates for Coolify. It will be done automatically behind the scenes, if there is no build process running.",
|
"auto_update_enabled_explainer": "Enable automatic updates for Coolify. It will be done automatically behind the scenes, if there is no build process running.",
|
||||||
"generate_www_non_www_ssl": "It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-settings'>both DNS entries</span> set in advance.",
|
"generate_www_non_www_ssl": "It will generate certificates for both www and non-www. <br>You need to have <span class=' text-settings'>both DNS entries</span> set in advance.",
|
||||||
"is_dns_check_enabled": "DNS check enabled?",
|
"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."
|
"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."
|
||||||
},
|
},
|
||||||
@@ -324,9 +324,9 @@
|
|||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"member": "member(s)",
|
"member": "member(s)",
|
||||||
"root": "(root)",
|
"root": "(root)",
|
||||||
"invited_with_permissions": "Invited to <span class=\"font-bold text-pink-600\">{{teamName}}</span> with <span class=\"font-bold text-rose-600\">{{permission}}</span> permission.",
|
"invited_with_permissions": "Invited to <span class=\" text-settings\">{{teamName}}</span> with <span class=\" text-rose-600\">{{permission}}</span> permission.",
|
||||||
"members": "Members",
|
"members": "Members",
|
||||||
"root_team_explainer": "This is the <span class='text-red-500 font-bold'>root</span> team. That means members of this group can manage instance wide settings and have all the priviliges in Coolify (imagine like root user on Linux).",
|
"root_team_explainer": "This is the <span class='text-red-500 '>root</span> team. That means members of this group can manage instance wide settings and have all the priviliges in Coolify (imagine like root user on Linux).",
|
||||||
"permission": "Permission",
|
"permission": "Permission",
|
||||||
"you": "(You)",
|
"you": "(You)",
|
||||||
"promote_to": "Promote to {{grade}}",
|
"promote_to": "Promote to {{grade}}",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import cuid from 'cuid';
|
|||||||
import { writable, readable, type Writable } from 'svelte/store';
|
import { writable, readable, type Writable } from 'svelte/store';
|
||||||
|
|
||||||
interface AppSession {
|
interface AppSession {
|
||||||
registrationEnabled: boolean;
|
isRegistrationEnabled: boolean;
|
||||||
ipv4: string | null,
|
ipv4: string | null,
|
||||||
ipv6: string | null,
|
ipv6: string | null,
|
||||||
version: string | null,
|
version: string | null,
|
||||||
@@ -26,8 +26,12 @@ interface AddToast {
|
|||||||
message: string,
|
message: string,
|
||||||
timeout?: number | undefined
|
timeout?: number | undefined
|
||||||
}
|
}
|
||||||
|
export const updateLoading: Writable<boolean> = writable(false);
|
||||||
|
export const isUpdateAvailable: Writable<boolean> = writable(false);
|
||||||
|
export const search: any = writable('')
|
||||||
export const loginEmail: Writable<string | undefined> = writable()
|
export const loginEmail: Writable<string | undefined> = writable()
|
||||||
export const appSession: Writable<AppSession> = writable({
|
export const appSession: Writable<AppSession> = writable({
|
||||||
|
isRegistrationEnabled: false,
|
||||||
ipv4: null,
|
ipv4: null,
|
||||||
ipv6: null,
|
ipv6: null,
|
||||||
version: null,
|
version: null,
|
||||||
@@ -70,6 +74,7 @@ export const status: Writable<any> = writable({
|
|||||||
application: {
|
application: {
|
||||||
isRunning: false,
|
isRunning: false,
|
||||||
isExited: false,
|
isExited: false,
|
||||||
|
isRestarting: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
initialLoading: true
|
initialLoading: true
|
||||||
},
|
},
|
||||||
@@ -83,7 +88,8 @@ export const status: Writable<any> = writable({
|
|||||||
isRunning: false,
|
isRunning: false,
|
||||||
isExited: false,
|
isExited: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
initialLoading: true
|
initialLoading: true,
|
||||||
|
isPublic: false
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -151,4 +157,6 @@ export const addToast = (toast: AddToast) => {
|
|||||||
let t: any = { ...defaults, ...toast }
|
let t: any = { ...defaults, ...toast }
|
||||||
if (t.timeout) t.timeoutInterval = setTimeout(() => dismissToast(id), t.timeout)
|
if (t.timeout) t.timeoutInterval = setTimeout(() => dismissToast(id), t.timeout)
|
||||||
toasts.update((all: any) => [t, ...all])
|
toasts.update((all: any) => [t, ...all])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const selectedBuildId: any = writable(null)
|
||||||
@@ -195,6 +195,7 @@ export function findBuildPack(pack: string, packageManager = 'npm') {
|
|||||||
export const buildPacks = [
|
export const buildPacks = [
|
||||||
{
|
{
|
||||||
name: 'node',
|
name: 'node',
|
||||||
|
type: 'base',
|
||||||
fancyName: 'Node.js',
|
fancyName: 'Node.js',
|
||||||
hoverColor: 'hover:bg-green-700',
|
hoverColor: 'hover:bg-green-700',
|
||||||
color: 'bg-green-700',
|
color: 'bg-green-700',
|
||||||
@@ -202,6 +203,7 @@ export const buildPacks = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'static',
|
name: 'static',
|
||||||
|
type: 'base',
|
||||||
fancyName: 'Static',
|
fancyName: 'Static',
|
||||||
hoverColor: 'hover:bg-orange-700',
|
hoverColor: 'hover:bg-orange-700',
|
||||||
color: 'bg-orange-700',
|
color: 'bg-orange-700',
|
||||||
@@ -210,6 +212,7 @@ export const buildPacks = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
name: 'php',
|
name: 'php',
|
||||||
|
type: 'base',
|
||||||
fancyName: 'PHP',
|
fancyName: 'PHP',
|
||||||
hoverColor: 'hover:bg-indigo-700',
|
hoverColor: 'hover:bg-indigo-700',
|
||||||
color: 'bg-indigo-700',
|
color: 'bg-indigo-700',
|
||||||
@@ -217,6 +220,8 @@ export const buildPacks = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'laravel',
|
name: 'laravel',
|
||||||
|
type: 'specific',
|
||||||
|
base: 'php',
|
||||||
fancyName: 'Laravel',
|
fancyName: 'Laravel',
|
||||||
hoverColor: 'hover:bg-indigo-700',
|
hoverColor: 'hover:bg-indigo-700',
|
||||||
color: 'bg-indigo-700',
|
color: 'bg-indigo-700',
|
||||||
@@ -224,6 +229,7 @@ export const buildPacks = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'docker',
|
name: 'docker',
|
||||||
|
type: 'base',
|
||||||
fancyName: 'Docker',
|
fancyName: 'Docker',
|
||||||
hoverColor: 'hover:bg-sky-700',
|
hoverColor: 'hover:bg-sky-700',
|
||||||
color: 'bg-sky-700',
|
color: 'bg-sky-700',
|
||||||
@@ -231,6 +237,8 @@ export const buildPacks = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'svelte',
|
name: 'svelte',
|
||||||
|
type: 'specific',
|
||||||
|
base: 'node',
|
||||||
fancyName: 'Svelte',
|
fancyName: 'Svelte',
|
||||||
hoverColor: 'hover:bg-orange-700',
|
hoverColor: 'hover:bg-orange-700',
|
||||||
color: 'bg-orange-700',
|
color: 'bg-orange-700',
|
||||||
@@ -238,6 +246,8 @@ export const buildPacks = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'vuejs',
|
name: 'vuejs',
|
||||||
|
type: 'specific',
|
||||||
|
base: 'node',
|
||||||
fancyName: 'VueJS',
|
fancyName: 'VueJS',
|
||||||
hoverColor: 'hover:bg-green-700',
|
hoverColor: 'hover:bg-green-700',
|
||||||
color: 'bg-green-700',
|
color: 'bg-green-700',
|
||||||
@@ -245,6 +255,8 @@ export const buildPacks = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'nuxtjs',
|
name: 'nuxtjs',
|
||||||
|
type: 'specific',
|
||||||
|
base: 'node',
|
||||||
fancyName: 'NuxtJS',
|
fancyName: 'NuxtJS',
|
||||||
hoverColor: 'hover:bg-green-700',
|
hoverColor: 'hover:bg-green-700',
|
||||||
color: 'bg-green-700',
|
color: 'bg-green-700',
|
||||||
@@ -252,6 +264,8 @@ export const buildPacks = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'gatsby',
|
name: 'gatsby',
|
||||||
|
type: 'specific',
|
||||||
|
base: 'node',
|
||||||
fancyName: 'Gatsby',
|
fancyName: 'Gatsby',
|
||||||
hoverColor: 'hover:bg-blue-700',
|
hoverColor: 'hover:bg-blue-700',
|
||||||
color: 'bg-blue-700',
|
color: 'bg-blue-700',
|
||||||
@@ -259,6 +273,8 @@ export const buildPacks = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'astro',
|
name: 'astro',
|
||||||
|
type: 'specific',
|
||||||
|
base: 'node',
|
||||||
fancyName: 'Astro',
|
fancyName: 'Astro',
|
||||||
hoverColor: 'hover:bg-pink-700',
|
hoverColor: 'hover:bg-pink-700',
|
||||||
color: 'bg-pink-700',
|
color: 'bg-pink-700',
|
||||||
@@ -266,14 +282,17 @@ export const buildPacks = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'eleventy',
|
name: 'eleventy',
|
||||||
|
type: 'specific',
|
||||||
|
base: 'node',
|
||||||
fancyName: 'Eleventy',
|
fancyName: 'Eleventy',
|
||||||
hoverColor: 'hover:bg-red-700',
|
hoverColor: 'hover:bg-red-700',
|
||||||
color: 'bg-red-700',
|
color: 'bg-red-700',
|
||||||
isCoolifyBuildPack: true,
|
isCoolifyBuildPack: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'react',
|
name: 'react',
|
||||||
|
type: 'specific',
|
||||||
|
base: 'node',
|
||||||
fancyName: 'React',
|
fancyName: 'React',
|
||||||
hoverColor: 'hover:bg-blue-700',
|
hoverColor: 'hover:bg-blue-700',
|
||||||
color: 'bg-blue-700',
|
color: 'bg-blue-700',
|
||||||
@@ -281,6 +300,8 @@ export const buildPacks = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'preact',
|
name: 'preact',
|
||||||
|
type: 'specific',
|
||||||
|
base: 'node',
|
||||||
fancyName: 'Preact',
|
fancyName: 'Preact',
|
||||||
hoverColor: 'hover:bg-blue-700',
|
hoverColor: 'hover:bg-blue-700',
|
||||||
color: 'bg-blue-700',
|
color: 'bg-blue-700',
|
||||||
@@ -288,6 +309,8 @@ export const buildPacks = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'nextjs',
|
name: 'nextjs',
|
||||||
|
type: 'specific',
|
||||||
|
base: 'node',
|
||||||
fancyName: 'NextJS',
|
fancyName: 'NextJS',
|
||||||
hoverColor: 'hover:bg-blue-700',
|
hoverColor: 'hover:bg-blue-700',
|
||||||
color: 'bg-blue-700',
|
color: 'bg-blue-700',
|
||||||
@@ -295,6 +318,8 @@ export const buildPacks = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'nestjs',
|
name: 'nestjs',
|
||||||
|
type: 'specific',
|
||||||
|
base: 'node',
|
||||||
fancyName: 'NestJS',
|
fancyName: 'NestJS',
|
||||||
hoverColor: 'hover:bg-red-700',
|
hoverColor: 'hover:bg-red-700',
|
||||||
color: 'bg-red-700',
|
color: 'bg-red-700',
|
||||||
@@ -302,6 +327,7 @@ export const buildPacks = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'rust',
|
name: 'rust',
|
||||||
|
type: 'base',
|
||||||
fancyName: 'Rust',
|
fancyName: 'Rust',
|
||||||
hoverColor: 'hover:bg-pink-700',
|
hoverColor: 'hover:bg-pink-700',
|
||||||
color: 'bg-pink-700',
|
color: 'bg-pink-700',
|
||||||
@@ -309,6 +335,7 @@ export const buildPacks = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'python',
|
name: 'python',
|
||||||
|
type: 'base',
|
||||||
fancyName: 'Python',
|
fancyName: 'Python',
|
||||||
hoverColor: 'hover:bg-green-700',
|
hoverColor: 'hover:bg-green-700',
|
||||||
color: 'bg-green-700',
|
color: 'bg-green-700',
|
||||||
@@ -316,6 +343,7 @@ export const buildPacks = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'deno',
|
name: 'deno',
|
||||||
|
type: 'base',
|
||||||
fancyName: 'Deno',
|
fancyName: 'Deno',
|
||||||
hoverColor: 'hover:bg-green-700',
|
hoverColor: 'hover:bg-green-700',
|
||||||
color: 'bg-green-700',
|
color: 'bg-green-700',
|
||||||
@@ -323,6 +351,7 @@ export const buildPacks = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'heroku',
|
name: 'heroku',
|
||||||
|
type: 'base',
|
||||||
fancyName: 'Heroku',
|
fancyName: 'Heroku',
|
||||||
hoverColor: 'hover:bg-purple-700',
|
hoverColor: 'hover:bg-purple-700',
|
||||||
color: 'bg-purple-700',
|
color: 'bg-purple-700',
|
||||||
|
|||||||
150
apps/ui/src/routes/_NewResource.svelte
Normal file
150
apps/ui/src/routes/_NewResource.svelte
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { post } from '$lib/api';
|
||||||
|
|
||||||
|
async function newApplication() {
|
||||||
|
const { id } = await post('/applications/new', {});
|
||||||
|
return await goto(`/applications/${id}`, { replaceState: true });
|
||||||
|
}
|
||||||
|
async function newService() {
|
||||||
|
const { id } = await post('/services/new', {});
|
||||||
|
return await goto(`/services/${id}`, { replaceState: true });
|
||||||
|
}
|
||||||
|
async function newDatabase() {
|
||||||
|
const { id } = await post('/databases/new', {});
|
||||||
|
return await goto(`/databases/${id}`, { replaceState: true });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="dropdown dropdown-bottom">
|
||||||
|
<slot>
|
||||||
|
<label for="new" tabindex="0" class="btn btn-square btn-sm bg-coollabs">
|
||||||
|
<svg
|
||||||
|
class="h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
><path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||||
|
/></svg
|
||||||
|
></label
|
||||||
|
>
|
||||||
|
</slot>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
id="new"
|
||||||
|
tabindex="0"
|
||||||
|
class="dropdown-content menu p-2 shadow bg-coolgray-300 rounded w-52"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<button on:click={newApplication} class="no-underline hover:bg-applications rounded-none ">
|
||||||
|
<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="4" width="6" height="6" rx="1" />
|
||||||
|
<rect x="4" y="14" width="6" height="6" rx="1" />
|
||||||
|
<rect x="14" y="14" width="6" height="6" rx="1" />
|
||||||
|
<line x1="14" y1="7" x2="20" y2="7" />
|
||||||
|
<line x1="17" y1="4" x2="17" y2="10" />
|
||||||
|
</svg>Application</button
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button on:click={newService} class="no-underline hover:bg-services rounded-none ">
|
||||||
|
<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="M7 18a4.6 4.4 0 0 1 0 -9a5 4.5 0 0 1 11 2h1a3.5 3.5 0 0 1 0 7h-12" />
|
||||||
|
</svg>Service</button
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button on:click={newDatabase} class="no-underline hover:bg-databases rounded-none ">
|
||||||
|
<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" />
|
||||||
|
<ellipse cx="12" cy="6" rx="8" ry="3" />
|
||||||
|
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
|
||||||
|
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
|
||||||
|
</svg>Database</button
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/sources/new" class="no-underline hover:bg-sources rounded-none ">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-6 w-6"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<circle cx="6" cy="6" r="2" />
|
||||||
|
<circle cx="18" cy="18" r="2" />
|
||||||
|
<path d="M11 6h5a2 2 0 0 1 2 2v8" />
|
||||||
|
<polyline points="14 9 11 6 14 3" />
|
||||||
|
<path d="M13 18h-5a2 2 0 0 1 -2 -2v-8" />
|
||||||
|
<polyline points="10 15 13 18 10 21" />
|
||||||
|
</svg>Git Source</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/destinations/new" class="no-underline hover:bg-destinations rounded-none ">
|
||||||
|
<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="M22 12.54c-1.804 -.345 -2.701 -1.08 -3.523 -2.94c-.487 .696 -1.102 1.568 -.92 2.4c.028 .238 -.32 1.002 -.557 1h-14c0 5.208 3.164 7 6.196 7c4.124 .022 7.828 -1.376 9.854 -5c1.146 -.101 2.296 -1.505 2.95 -2.46z"
|
||||||
|
/>
|
||||||
|
<path d="M5 10h3v3h-3z" />
|
||||||
|
<path d="M8 10h3v3h-3z" />
|
||||||
|
<path d="M11 10h3v3h-3z" />
|
||||||
|
<path d="M8 7h3v3h-3z" />
|
||||||
|
<path d="M11 7h3v3h-3z" />
|
||||||
|
<path d="M11 4h3v3h-3z" />
|
||||||
|
<path d="M4.571 18c1.5 0 2.047 -.074 2.958 -.78" />
|
||||||
|
<line x1="10" y1="16" x2="10" y2="16.01" />
|
||||||
|
</svg>Destination</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let baseSettings: any;
|
export let baseSettings: any;
|
||||||
export let supportedServiceTypesAndVersions: any;
|
export let supportedServiceTypesAndVersions: any;
|
||||||
$appSession.registrationEnabled = baseSettings.registrationEnabled;
|
$appSession.isRegistrationEnabled = baseSettings.isRegistrationEnabled;
|
||||||
$appSession.ipv4 = baseSettings.ipv4;
|
$appSession.ipv4 = baseSettings.ipv4;
|
||||||
$appSession.ipv6 = baseSettings.ipv6;
|
$appSession.ipv6 = baseSettings.ipv6;
|
||||||
$appSession.version = baseSettings.version;
|
$appSession.version = baseSettings.version;
|
||||||
@@ -107,10 +107,11 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Coolify</title>
|
|
||||||
{#if !$appSession.whiteLabeled}
|
{#if !$appSession.whiteLabeled}
|
||||||
|
<title>Coolify</title>
|
||||||
<link rel="icon" href="/favicon.png" />
|
<link rel="icon" href="/favicon.png" />
|
||||||
{:else if $appSession.whiteLabeledDetails.icon}
|
{:else if $appSession.whiteLabeledDetails.icon}
|
||||||
|
<title>Coolify</title>
|
||||||
<link rel="icon" href={$appSession.whiteLabeledDetails.icon} />
|
<link rel="icon" href={$appSession.whiteLabeledDetails.icon} />
|
||||||
{/if}
|
{/if}
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
@@ -120,30 +121,208 @@
|
|||||||
<PageLoader />
|
<PageLoader />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $appSession.userId}
|
<div class="drawer">
|
||||||
<nav class="nav-main">
|
<input id="main-drawer" type="checkbox" class="drawer-toggle" />
|
||||||
<div class="flex h-screen w-full flex-col items-center transition-all duration-100">
|
<div class="drawer-content">
|
||||||
{#if !$appSession.whiteLabeled}
|
{#if $appSession.userId}
|
||||||
<div class="mb-2 mt-4 h-10 w-10">
|
<nav class="nav-main hidden lg:block z-20">
|
||||||
<img src="/favicon.png" alt="coolLabs logo" />
|
<div class="flex h-screen w-full flex-col items-center transition-all duration-100">
|
||||||
</div>
|
{#if !$appSession.whiteLabeled}
|
||||||
{:else if $appSession.whiteLabeledDetails.icon}
|
<div class="mb-2 mt-4 h-10 w-10">
|
||||||
<div class="mb-2 mt-4 h-10 w-10">
|
<img src="/favicon.png" alt="coolLabs logo" />
|
||||||
<img src={$appSession.whiteLabeledDetails.icon} alt="White labeled logo" />
|
</div>
|
||||||
|
{:else if $appSession.whiteLabeledDetails.icon}
|
||||||
|
<div class="mb-2 mt-4 h-10 w-10">
|
||||||
|
<img src={$appSession.whiteLabeledDetails.icon} alt="White labeled logo" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="flex flex-col space-y-2 py-2" class:mt-2={$appSession.whiteLabeled}>
|
||||||
|
<a
|
||||||
|
id="dashboard"
|
||||||
|
sveltekit:prefetch
|
||||||
|
href="/"
|
||||||
|
class="icons hover:text-pink-500"
|
||||||
|
class:text-pink-500={$page.url.pathname === '/'}
|
||||||
|
class:bg-coolgray-500={$page.url.pathname === '/'}
|
||||||
|
class:bg-coolgray-200={!($page.url.pathname === '/')}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-9 w-9"
|
||||||
|
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="M19 8.71l-5.333 -4.148a2.666 2.666 0 0 0 -3.274 0l-5.334 4.148a2.665 2.665 0 0 0 -1.029 2.105v7.2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-7.2c0 -.823 -.38 -1.6 -1.03 -2.105"
|
||||||
|
/>
|
||||||
|
<path d="M16 15c-2.21 1.333 -5.792 1.333 -8 0" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
{#if $appSession.teamId === '0'}
|
||||||
|
<a
|
||||||
|
id="servers"
|
||||||
|
sveltekit:prefetch
|
||||||
|
href="/servers"
|
||||||
|
class="icons hover:text-sky-500"
|
||||||
|
class:text-sky-500={$page.url.pathname === '/servers'}
|
||||||
|
class:bg-coolgray-500={$page.url.pathname === '/servers'}
|
||||||
|
class:bg-coolgray-200={!($page.url.pathname === '/servers')}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="w-8 h-8 mx-auto"
|
||||||
|
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="3" y="4" width="18" height="8" rx="3" />
|
||||||
|
<rect x="3" y="12" width="18" height="8" rx="3" />
|
||||||
|
<line x1="7" y1="8" x2="7" y2="8.01" />
|
||||||
|
<line x1="7" y1="16" x2="7" y2="16.01" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<Tooltip triggeredBy="#dashboard" placement="right">Dashboard</Tooltip>
|
||||||
|
<Tooltip triggeredBy="#servers" placement="right">Servers</Tooltip>
|
||||||
|
<div class="flex-1" />
|
||||||
|
<div class="lg:block hidden">
|
||||||
|
<UpdateAvailable />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col space-y-2 py-2">
|
||||||
|
<a
|
||||||
|
id="iam"
|
||||||
|
sveltekit:prefetch
|
||||||
|
href="/iam"
|
||||||
|
class="icons hover:text-iam"
|
||||||
|
class:text-iam={$page.url.pathname.startsWith('/iam')}
|
||||||
|
class:bg-coolgray-500={$page.url.pathname.startsWith('/iam')}
|
||||||
|
><svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
class="h-9 w-9"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<circle cx="9" cy="7" r="4" />
|
||||||
|
<path d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2" />
|
||||||
|
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||||
|
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
id="settings"
|
||||||
|
sveltekit:prefetch
|
||||||
|
href={$appSession.teamId === '0' ? '/settings/coolify' : '/settings/ssh'}
|
||||||
|
class="icons hover:text-settings"
|
||||||
|
class:text-settings={$page.url.pathname.startsWith('/settings')}
|
||||||
|
class:bg-coolgray-500={$page.url.pathname.startsWith('/settings')}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
class="h-9 w-9"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path
|
||||||
|
d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z"
|
||||||
|
/>
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="logout"
|
||||||
|
class="icons bg-coolgray-200 hover:text-error cursor-pointer"
|
||||||
|
on:click={logout}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="ml-1 h-8 w-8"
|
||||||
|
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="M14 8v-2a2 2 0 0 0 -2 -2h-7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2 -2v-2"
|
||||||
|
/>
|
||||||
|
<path d="M7 12h14l-3 -3m0 6l3 -3" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="w-full text-center font-bold text-stone-400 hover:bg-coolgray-200 hover:text-white"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="text-[10px] no-underline"
|
||||||
|
href={`https://github.com/coollabsio/coolify/releases/tag/v${$appSession.version}`}
|
||||||
|
target="_blank">v{$appSession.version}</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</nav>
|
||||||
|
{#if $appSession.whiteLabeled}
|
||||||
|
<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}
|
{/if}
|
||||||
<div class="flex flex-col space-y-2 py-2" class:mt-2={$appSession.whiteLabeled}>
|
{/if}
|
||||||
|
<div
|
||||||
|
class="navbar lg:hidden space-x-2 flex flex-row items-center bg-coollabs"
|
||||||
|
class:hidden={!$appSession.userId}
|
||||||
|
>
|
||||||
|
<label for="main-drawer" class="drawer-button btn btn-square btn-ghost flex-col">
|
||||||
|
<span class="burger bg-white" />
|
||||||
|
<span class="burger bg-white" />
|
||||||
|
<span class="burger bg-white" />
|
||||||
|
</label>
|
||||||
|
<div class="prose flex flex-row justify-between space-x-1 w-full items-center pr-3">
|
||||||
|
{#if !$appSession.whiteLabeled}
|
||||||
|
<h3 class="mb-0 text-white">Coolify</h3>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<main>
|
||||||
|
<div class={$appSession.userId ? 'lg:pl-16' : null}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<div class="drawer-side">
|
||||||
|
<label for="main-drawer" class="drawer-overlay w-full" />
|
||||||
|
<ul class="menu bg-coolgray-200 w-60 p-2 space-y-3 pt-4 ">
|
||||||
|
<li>
|
||||||
<a
|
<a
|
||||||
id="dashboard"
|
class="no-underline icons hover:text-white hover:bg-pink-500"
|
||||||
sveltekit:prefetch
|
sveltekit:prefetch
|
||||||
href="/"
|
href="/"
|
||||||
class="icons bg-coolgray-200 hover:text-white"
|
class:bg-pink-500={$page.url.pathname === '/'}
|
||||||
class:text-white={$page.url.pathname === '/'}
|
|
||||||
class:bg-coolgray-500={$page.url.pathname === '/'}
|
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-9 w-9"
|
class="h-8 w-8"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -157,53 +336,20 @@
|
|||||||
/>
|
/>
|
||||||
<path d="M16 15c-2.21 1.333 -5.792 1.333 -8 0" />
|
<path d="M16 15c-2.21 1.333 -5.792 1.333 -8 0" />
|
||||||
</svg>
|
</svg>
|
||||||
|
Dashboard
|
||||||
</a>
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<div class="border-t border-stone-700" />
|
<li>
|
||||||
<a
|
<a
|
||||||
id="applications"
|
class="no-underline icons hover:text-white hover:bg-sky-500"
|
||||||
sveltekit:prefetch
|
sveltekit:prefetch
|
||||||
href="/applications"
|
href="/servers"
|
||||||
class="icons bg-coolgray-200"
|
class:bg-sky-500={$page.url.pathname.startsWith('/servers')}
|
||||||
class:text-applications={$page.url.pathname.startsWith('/applications') ||
|
|
||||||
$page.url.pathname.startsWith('/new/application')}
|
|
||||||
class:bg-coolgray-500={$page.url.pathname.startsWith('/applications') ||
|
|
||||||
$page.url.pathname.startsWith('/new/application')}
|
|
||||||
data-tip="Applications"
|
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-9 w-9"
|
class="w-8 h-8"
|
||||||
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="4" width="6" height="6" rx="1" />
|
|
||||||
<rect x="4" y="14" width="6" height="6" rx="1" />
|
|
||||||
<rect x="14" y="14" width="6" height="6" rx="1" />
|
|
||||||
<line x1="14" y1="7" x2="20" y2="7" />
|
|
||||||
<line x1="17" y1="4" x2="17" y2="10" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
|
||||||
id="sources"
|
|
||||||
sveltekit:prefetch
|
|
||||||
href="/sources"
|
|
||||||
class="icons bg-coolgray-200"
|
|
||||||
class:text-sources={$page.url.pathname.startsWith('/sources') ||
|
|
||||||
$page.url.pathname.startsWith('/new/source')}
|
|
||||||
class:bg-coolgray-500={$page.url.pathname.startsWith('/sources') ||
|
|
||||||
$page.url.pathname.startsWith('/new/source')}
|
|
||||||
data-tip="Git Sources"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-9 w-9"
|
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -212,118 +358,23 @@
|
|||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
>
|
>
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
<circle cx="6" cy="6" r="2" />
|
<rect x="3" y="4" width="18" height="8" rx="3" />
|
||||||
<circle cx="18" cy="18" r="2" />
|
<rect x="3" y="12" width="18" height="8" rx="3" />
|
||||||
<path d="M11 6h5a2 2 0 0 1 2 2v8" />
|
<line x1="7" y1="8" x2="7" y2="8.01" />
|
||||||
<polyline points="14 9 11 6 14 3" />
|
<line x1="7" y1="16" x2="7" y2="16.01" />
|
||||||
<path d="M13 18h-5a2 2 0 0 1 -2 -2v-8" />
|
|
||||||
<polyline points="10 15 13 18 10 21" />
|
|
||||||
</svg>
|
</svg>
|
||||||
|
Servers
|
||||||
</a>
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
<a
|
<a
|
||||||
id="destinations"
|
class="no-underline icons hover:text-white hover:bg-iam"
|
||||||
sveltekit:prefetch
|
|
||||||
href="/destinations"
|
|
||||||
class="icons bg-coolgray-200"
|
|
||||||
class:text-destinations={$page.url.pathname.startsWith('/destinations') ||
|
|
||||||
$page.url.pathname.startsWith('/new/destination')}
|
|
||||||
class:bg-coolgray-500={$page.url.pathname.startsWith('/destinations') ||
|
|
||||||
$page.url.pathname.startsWith('/new/destination')}
|
|
||||||
data-tip="Destinations"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-9 w-9"
|
|
||||||
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="M22 12.54c-1.804 -.345 -2.701 -1.08 -3.523 -2.94c-.487 .696 -1.102 1.568 -.92 2.4c.028 .238 -.32 1.002 -.557 1h-14c0 5.208 3.164 7 6.196 7c4.124 .022 7.828 -1.376 9.854 -5c1.146 -.101 2.296 -1.505 2.95 -2.46z"
|
|
||||||
/>
|
|
||||||
<path d="M5 10h3v3h-3z" />
|
|
||||||
<path d="M8 10h3v3h-3z" />
|
|
||||||
<path d="M11 10h3v3h-3z" />
|
|
||||||
<path d="M8 7h3v3h-3z" />
|
|
||||||
<path d="M11 7h3v3h-3z" />
|
|
||||||
<path d="M11 4h3v3h-3z" />
|
|
||||||
<path d="M4.571 18c1.5 0 2.047 -.074 2.958 -.78" />
|
|
||||||
<line x1="10" y1="16" x2="10" y2="16.01" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<div class="border-t border-stone-700" />
|
|
||||||
<a
|
|
||||||
id="databases"
|
|
||||||
sveltekit:prefetch
|
|
||||||
href="/databases"
|
|
||||||
class="icons bg-coolgray-200"
|
|
||||||
class:text-databases={$page.url.pathname.startsWith('/databases') ||
|
|
||||||
$page.url.pathname.startsWith('/new/database')}
|
|
||||||
class:bg-coolgray-500={$page.url.pathname.startsWith('/databases') ||
|
|
||||||
$page.url.pathname.startsWith('/new/database')}
|
|
||||||
data-tip="Databases"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-9 w-9"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
fill="none"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
>
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
||||||
<ellipse cx="12" cy="6" rx="8" ry="3" />
|
|
||||||
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
|
|
||||||
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
id="services"
|
|
||||||
sveltekit:prefetch
|
|
||||||
href="/services"
|
|
||||||
class="icons bg-coolgray-200"
|
|
||||||
class:text-services={$page.url.pathname.startsWith('/services') ||
|
|
||||||
$page.url.pathname.startsWith('/new/service')}
|
|
||||||
class:bg-coolgray-500={$page.url.pathname.startsWith('/services') ||
|
|
||||||
$page.url.pathname.startsWith('/new/service')}
|
|
||||||
data-tip="Services"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-9 w-9"
|
|
||||||
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="M7 18a4.6 4.4 0 0 1 0 -9a5 4.5 0 0 1 11 2h1a3.5 3.5 0 0 1 0 7h-12" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1" />
|
|
||||||
|
|
||||||
<UpdateAvailable />
|
|
||||||
<div class="flex flex-col space-y-2 py-2">
|
|
||||||
<a
|
|
||||||
id="iam"
|
|
||||||
sveltekit:prefetch
|
|
||||||
href="/iam"
|
href="/iam"
|
||||||
class="icons bg-coolgray-200"
|
class:bg-iam={$page.url.pathname.startsWith('/iam')}
|
||||||
class:text-iam={$page.url.pathname.startsWith('/iam')}
|
|
||||||
class:bg-coolgray-500={$page.url.pathname.startsWith('/iam')}
|
|
||||||
><svg
|
><svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
class="h-9 w-9"
|
class="h-8 w-8"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -336,19 +387,20 @@
|
|||||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||||
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85" />
|
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85" />
|
||||||
</svg>
|
</svg>
|
||||||
|
IAM
|
||||||
</a>
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
<a
|
<a
|
||||||
id="settings"
|
class="no-underline icons hover:text-black hover:bg-settings"
|
||||||
sveltekit:prefetch
|
href={$appSession.teamId === '0' ? '/settings/coolify' : '/settings/ssh'}
|
||||||
href={$appSession.teamId === '0' ? '/settings/global' : '/settings/ssh-keys'}
|
class:bg-settings={$page.url.pathname.startsWith('/settings')}
|
||||||
class="icons bg-coolgray-200"
|
class:text-black={$page.url.pathname.startsWith('/settings')}
|
||||||
class:text-settings={$page.url.pathname.startsWith('/settings')}
|
|
||||||
class:bg-coolgray-500={$page.url.pathname.startsWith('/settings')}
|
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
class="h-9 w-9"
|
class="h-8 w-8"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -361,13 +413,15 @@
|
|||||||
/>
|
/>
|
||||||
<circle cx="12" cy="12" r="3" />
|
<circle cx="12" cy="12" r="3" />
|
||||||
</svg>
|
</svg>
|
||||||
|
Settings
|
||||||
</a>
|
</a>
|
||||||
|
</li>
|
||||||
<div
|
<li class="flex-1 bg-transparent" />
|
||||||
id="logout"
|
<div class="block lg:hidden">
|
||||||
class="icons bg-coolgray-200 hover:text-error"
|
<UpdateAvailable />
|
||||||
on:click={logout}
|
</div>
|
||||||
>
|
<li>
|
||||||
|
<div class="no-underline icons hover:bg-error" on:click={logout}>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="ml-1 h-8 w-8"
|
class="ml-1 h-8 w-8"
|
||||||
@@ -384,40 +438,20 @@
|
|||||||
/>
|
/>
|
||||||
<path d="M7 12h14l-3 -3m0 6l3 -3" />
|
<path d="M7 12h14l-3 -3m0 6l3 -3" />
|
||||||
</svg>
|
</svg>
|
||||||
|
<div class="-ml-1">Logout</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
</li>
|
||||||
class="w-full text-center font-bold text-stone-400 hover:bg-coolgray-200 hover:text-white"
|
<li class="w-full">
|
||||||
|
<a
|
||||||
|
class="text-xs hover:bg-coolgray-200 no-underline hover:text-white text-right"
|
||||||
|
href={`https://github.com/coollabsio/coolify/releases/tag/v${$appSession.version}`}
|
||||||
|
target="_blank">v{$appSession.version}</a
|
||||||
>
|
>
|
||||||
<a
|
</li>
|
||||||
class="text-[10px] no-underline"
|
</ul>
|
||||||
href={`https://github.com/coollabsio/coolify/releases/tag/v${$appSession.version}`}
|
|
||||||
target="_blank">v{$appSession.version}</a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
{#if $appSession.whiteLabeled}
|
|
||||||
<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}
|
|
||||||
{/if}
|
|
||||||
<main>
|
|
||||||
<div class={$appSession.userId ? 'pl-14 lg:px-20' : null}>
|
|
||||||
<slot />
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</div>
|
||||||
|
|
||||||
<Tooltip triggeredBy="#dashboard" placement="right">Dashboard</Tooltip>
|
|
||||||
<Tooltip triggeredBy="#applications" placement="right" color="bg-applications">Applications</Tooltip
|
|
||||||
>
|
|
||||||
<Tooltip triggeredBy="#sources" placement="right" color="bg-sources">Git Sources</Tooltip>
|
|
||||||
<Tooltip triggeredBy="#destinations" placement="right" color="bg-destinations">Destinations</Tooltip
|
|
||||||
>
|
|
||||||
<Tooltip triggeredBy="#databases" placement="right" color="bg-databases">Databases</Tooltip>
|
|
||||||
<Tooltip triggeredBy="#services" placement="right" color="bg-services">Services</Tooltip>
|
|
||||||
<Tooltip triggeredBy="#iam" placement="right" color="bg-iam">IAM</Tooltip>
|
<Tooltip triggeredBy="#iam" placement="right" color="bg-iam">IAM</Tooltip>
|
||||||
<Tooltip triggeredBy="#settings" placement="right" color="bg-settings text-black">Settings</Tooltip
|
<Tooltip triggeredBy="#settings" placement="right" color="bg-settings text-black">Settings</Tooltip>
|
||||||
>
|
|
||||||
<Tooltip triggeredBy="#logout" placement="right" color="bg-red-600">Logout</Tooltip>
|
<Tooltip triggeredBy="#logout" placement="right" color="bg-red-600">Logout</Tooltip>
|
||||||
|
|||||||
289
apps/ui/src/routes/applications/[id]/_Menu.svelte
Normal file
289
apps/ui/src/routes/applications/[id]/_Menu.svelte
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let application: any;
|
||||||
|
import { status } from '$lib/store';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ul class="menu border bg-coolgray-100 border-coolgray-200 rounded p-2 space-y-2 sticky top-4">
|
||||||
|
<li class="menu-title">
|
||||||
|
<span>Configuration</span>
|
||||||
|
</li>
|
||||||
|
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
id="git"
|
||||||
|
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
|
||||||
|
target="_blank"
|
||||||
|
class="no-underline"
|
||||||
|
>
|
||||||
|
{#if application.gitSource?.type === 'gitlab'}
|
||||||
|
<svg viewBox="0 0 128 128" class="w-6 h-6">
|
||||||
|
<path
|
||||||
|
fill="#FC6D26"
|
||||||
|
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
|
||||||
|
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
|
||||||
|
fill="#FC6D26"
|
||||||
|
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
|
||||||
|
/><path
|
||||||
|
fill="#FCA326"
|
||||||
|
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
|
||||||
|
/><path
|
||||||
|
fill="#E24329"
|
||||||
|
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
|
||||||
|
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
|
||||||
|
fill="#FCA326"
|
||||||
|
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
|
||||||
|
/><path
|
||||||
|
fill="#E24329"
|
||||||
|
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{:else if application.gitSource?.type === 'github'}
|
||||||
|
<svg viewBox="0 0 128 128" class="w-6 h-6">
|
||||||
|
<g fill="#ffffff"
|
||||||
|
><path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
|
||||||
|
/><path
|
||||||
|
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
|
||||||
|
/></g
|
||||||
|
>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
Open on Git
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="3"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="w-3 h-3 text-white"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M4.5 19.5l15-15m0 0H8.25m11.25 0v11.25"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<li class="rounded" class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}`}>
|
||||||
|
<a href={`/applications/${$page.params.id}`} class="no-underline w-full"
|
||||||
|
><svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="w-6 h-6"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path
|
||||||
|
d="M7 10h3v-3l-3.5 -3.5a6 6 0 0 1 8 8l6 6a2 2 0 0 1 -3 3l-6 -6a6 6 0 0 1 -8 -8l3.5 3.5"
|
||||||
|
/>
|
||||||
|
</svg>Build & Deploy</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="rounded"
|
||||||
|
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/secrets`}
|
||||||
|
>
|
||||||
|
<a href={`/applications/${$page.params.id}/secrets`} class="no-underline w-full"
|
||||||
|
><svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="w-6 h-6"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path
|
||||||
|
d="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3"
|
||||||
|
/>
|
||||||
|
<circle cx="12" cy="11" r="1" />
|
||||||
|
<line x1="12" y1="12" x2="12" y2="14.5" />
|
||||||
|
</svg>Secrets</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="rounded"
|
||||||
|
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/storages`}
|
||||||
|
>
|
||||||
|
<a href={`/applications/${$page.params.id}/storages`} class="no-underline w-full"
|
||||||
|
><svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="w-6 h-6"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<ellipse cx="12" cy="6" rx="8" ry="3" />
|
||||||
|
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
|
||||||
|
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
|
||||||
|
</svg>Persistent Volumes</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="rounded"
|
||||||
|
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/features`}
|
||||||
|
>
|
||||||
|
<a href={`/applications/${$page.params.id}/features`} class="no-underline w-full"
|
||||||
|
><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" />
|
||||||
|
<polyline points="13 3 13 10 19 10 11 21 11 14 5 14 13 3" />
|
||||||
|
</svg>Features</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="menu-title">
|
||||||
|
<span>Logs</span>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class:text-stone-600={!$status.application.isRunning}
|
||||||
|
class="rounded"
|
||||||
|
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/logs`}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={$status.application.isRunning ? `/applications/${$page.params.id}/logs` : ''}
|
||||||
|
class="no-underline w-full"
|
||||||
|
><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>Application</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="rounded"
|
||||||
|
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/logs/build`}
|
||||||
|
>
|
||||||
|
<a href={`/applications/${$page.params.id}/logs/build`} class="no-underline w-full"
|
||||||
|
><svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-6 w-6"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<circle cx="19" cy="13" r="2" />
|
||||||
|
<circle cx="4" cy="17" r="2" />
|
||||||
|
<circle cx="13" cy="17" r="2" />
|
||||||
|
<line x1="13" y1="19" x2="4" y2="19" />
|
||||||
|
<line x1="4" y1="15" x2="13" y2="15" />
|
||||||
|
<path d="M8 12v-5h2a3 3 0 0 1 3 3v5" />
|
||||||
|
<path d="M5 15v-2a1 1 0 0 1 1 -1h7" />
|
||||||
|
<path d="M19 11v-7l-6 7" />
|
||||||
|
</svg>Build</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li class="menu-title">
|
||||||
|
<span>Advanced</span>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="rounded"
|
||||||
|
class:text-stone-600={!$status.application.isRunning}
|
||||||
|
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/usage`}
|
||||||
|
>
|
||||||
|
<a href={$status.application.isRunning ? `/applications/${$page.params.id}/usage` : ''} class="no-underline w-full"
|
||||||
|
><svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="w-6 h-6"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M3 12h4l3 8l4 -16l3 8h4" />
|
||||||
|
</svg>Monitoring</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
{#if !application.settings.isBot}
|
||||||
|
<li
|
||||||
|
class="rounded"
|
||||||
|
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/previews`}
|
||||||
|
>
|
||||||
|
<a href={`/applications/${$page.params.id}/previews`} class="no-underline w-full"
|
||||||
|
><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="7" cy="18" r="2" />
|
||||||
|
<circle cx="7" cy="6" r="2" />
|
||||||
|
<circle cx="17" cy="12" r="2" />
|
||||||
|
<line x1="7" y1="8" x2="7" y2="16" />
|
||||||
|
<path d="M7 8a4 4 0 0 0 4 4h4" />
|
||||||
|
</svg>Preview Deployments</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
<li
|
||||||
|
class="rounded"
|
||||||
|
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/danger`}
|
||||||
|
>
|
||||||
|
<a href={`/applications/${$page.params.id}/danger`} class="no-underline w-full"
|
||||||
|
><svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="w-6 h-6"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M12 9v2m0 4v.01" />
|
||||||
|
<path
|
||||||
|
d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"
|
||||||
|
/>
|
||||||
|
</svg>Danger Zone</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
128
apps/ui/src/routes/applications/[id]/_PreviewSecret.svelte
Normal file
128
apps/ui/src/routes/applications/[id]/_PreviewSecret.svelte
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let length = 0;
|
||||||
|
export let index: number = 0;
|
||||||
|
export let name = '';
|
||||||
|
export let value = '';
|
||||||
|
export let isBuildSecret = false;
|
||||||
|
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { del, post, put } from '$lib/api';
|
||||||
|
import { errorNotification } from '$lib/common';
|
||||||
|
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||||
|
import { addToast } from '$lib/store';
|
||||||
|
import { t } from '$lib/translations';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
const { id } = $page.params;
|
||||||
|
|
||||||
|
async function updatePreviewSecret() {
|
||||||
|
try {
|
||||||
|
await put(`/applications/${id}/secrets/preview`, {
|
||||||
|
name,
|
||||||
|
value
|
||||||
|
});
|
||||||
|
addToast({
|
||||||
|
message: 'Secret updated.',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return errorNotification(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full font-bold grid grid-cols-1 lg:grid-cols-4 gap-2 pb-2">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
{#if index === 0 || length === 0}
|
||||||
|
<label for="name" class="pb-2 uppercase">name</label>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="secretName"
|
||||||
|
readonly
|
||||||
|
disabled
|
||||||
|
value={name}
|
||||||
|
required
|
||||||
|
placeholder="EXAMPLE_VARIABLE"
|
||||||
|
class=" w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
{#if index === 0 || length === 0}
|
||||||
|
<label for="value" class="pb-2 uppercase">value</label>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<CopyPasswordField
|
||||||
|
id="secretValue"
|
||||||
|
name="secretValue"
|
||||||
|
isPasswordField={true}
|
||||||
|
bind:value
|
||||||
|
placeholder="J$#@UIO%HO#$U%H"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex lg:flex-col flex-row justify-start items-center pt-3 lg:pt-0">
|
||||||
|
{#if index === 0 || length === 0}
|
||||||
|
<label for="name" class="pb-2 uppercase lg:block hidden">Need during buildtime?</label>
|
||||||
|
{/if}
|
||||||
|
<label for="name" class="pb-2 uppercase lg:hidden block">Need during buildtime?</label>
|
||||||
|
|
||||||
|
<div class="flex justify-center h-full items-center pt-0 lg:pt-0 pl-4 lg:pl-0">
|
||||||
|
<button
|
||||||
|
aria-pressed="false"
|
||||||
|
class="opacity-50 cursor-pointer cursor-not-allowedrelative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out "
|
||||||
|
class:bg-green-600={isBuildSecret}
|
||||||
|
class:bg-stone-700={!isBuildSecret}
|
||||||
|
>
|
||||||
|
<span class="sr-only">{$t('application.secrets.use_isbuildsecret')}</span>
|
||||||
|
<span
|
||||||
|
class="pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow transition duration-200 ease-in-out"
|
||||||
|
class:translate-x-5={isBuildSecret}
|
||||||
|
class:translate-x-0={!isBuildSecret}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
|
||||||
|
class:opacity-0={isBuildSecret}
|
||||||
|
class:opacity-100={!isBuildSecret}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<svg class="h-3 w-3 bg-white text-red-600" fill="none" viewBox="0 0 12 12">
|
||||||
|
<path
|
||||||
|
d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-100 ease-out"
|
||||||
|
aria-hidden="true"
|
||||||
|
class:opacity-100={isBuildSecret}
|
||||||
|
class:opacity-0={!isBuildSecret}
|
||||||
|
>
|
||||||
|
<svg class="h-3 w-3 bg-white text-green-600" fill="currentColor" viewBox="0 0 12 12">
|
||||||
|
<path
|
||||||
|
d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row lg:flex-col lg:items-center items-start">
|
||||||
|
{#if index === 0 || length === 0}
|
||||||
|
<label for="name" class="pb-2 uppercase lg:block hidden">Actions</label>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="flex justify-center h-full items-center pt-3">
|
||||||
|
<div class="flex flex-row justify-center space-x-2">
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<button class="btn btn-sm btn-primary" on:click={updatePreviewSecret}>Update</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,186 +1,189 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
export let length = 0;
|
||||||
|
export let index: number = 0;
|
||||||
export let name = '';
|
export let name = '';
|
||||||
export let value = '';
|
export let value = '';
|
||||||
export let isBuildSecret = false;
|
export let isBuildSecret = false;
|
||||||
export let isNewSecret = false;
|
export let isNewSecret = false;
|
||||||
export let isPRMRSecret = false;
|
|
||||||
export let PRMRSecret: any = {};
|
|
||||||
|
|
||||||
if (isPRMRSecret) value = PRMRSecret.value;
|
|
||||||
|
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { del } from '$lib/api';
|
import { del, post, put } from '$lib/api';
|
||||||
import { errorNotification } from '$lib/common';
|
import { errorNotification } from '$lib/common';
|
||||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||||
import { addToast } from '$lib/store';
|
import { addToast } from '$lib/store';
|
||||||
import { t } from '$lib/translations';
|
import { t } from '$lib/translations';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import { saveSecret } from './utils';
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
const { id } = $page.params;
|
const { id } = $page.params;
|
||||||
|
function cleanupState() {
|
||||||
|
if (isNewSecret) {
|
||||||
|
name = '';
|
||||||
|
value = '';
|
||||||
|
isBuildSecret = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
async function removeSecret() {
|
async function removeSecret() {
|
||||||
try {
|
try {
|
||||||
await del(`/applications/${id}/secrets`, { name });
|
await del(`/applications/${id}/secrets`, { name });
|
||||||
dispatch('refresh');
|
cleanupState();
|
||||||
if (isNewSecret) {
|
|
||||||
name = '';
|
|
||||||
value = '';
|
|
||||||
isBuildSecret = false;
|
|
||||||
}
|
|
||||||
addToast({
|
addToast({
|
||||||
message: 'Secret removed.',
|
message: 'Secret removed.',
|
||||||
type: 'success'
|
type: 'success'
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
return errorNotification(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createSecret(isNew: any) {
|
|
||||||
try {
|
|
||||||
if (!name || !value) return;
|
|
||||||
await saveSecret({
|
|
||||||
isNew,
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
isBuildSecret,
|
|
||||||
isPRMRSecret,
|
|
||||||
isNewSecret,
|
|
||||||
applicationId: id
|
|
||||||
});
|
|
||||||
if (isNewSecret) {
|
|
||||||
name = '';
|
|
||||||
value = '';
|
|
||||||
isBuildSecret = false;
|
|
||||||
addToast({
|
|
||||||
message: 'Secret added.',
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
addToast({
|
|
||||||
message: 'Secret updated.',
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
dispatch('refresh');
|
dispatch('refresh');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorNotification(error);
|
return errorNotification(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setSecretValue() {
|
async function addNewSecret() {
|
||||||
if (!isPRMRSecret) {
|
try {
|
||||||
isBuildSecret = !isBuildSecret;
|
if (!name) return errorNotification({ message: 'Name is required.' });
|
||||||
if (!isNewSecret) {
|
if (!value) return errorNotification({ message: 'Value is required.' });
|
||||||
await saveSecret({
|
await post(`/applications/${id}/secrets`, {
|
||||||
isNew: isNewSecret,
|
name,
|
||||||
name,
|
value,
|
||||||
value,
|
isBuildSecret
|
||||||
isBuildSecret,
|
});
|
||||||
isPRMRSecret,
|
cleanupState();
|
||||||
isNewSecret,
|
addToast({
|
||||||
applicationId: id
|
message: 'Secret added.',
|
||||||
});
|
type: 'success'
|
||||||
addToast({
|
});
|
||||||
message: 'Secret updated.',
|
dispatch('refresh');
|
||||||
type: 'success'
|
} catch (error) {
|
||||||
});
|
return errorNotification(error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateSecret({
|
||||||
|
changeIsBuildSecret = false
|
||||||
|
}: { changeIsBuildSecret?: boolean } = {}) {
|
||||||
|
if (changeIsBuildSecret) isBuildSecret = !isBuildSecret;
|
||||||
|
if (isNewSecret) return;
|
||||||
|
try {
|
||||||
|
await put(`/applications/${id}/secrets`, {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
isBuildSecret: changeIsBuildSecret ? isBuildSecret : undefined
|
||||||
|
});
|
||||||
|
addToast({
|
||||||
|
message: 'Secret updated.',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
dispatch('refresh');
|
||||||
|
} catch (error) {
|
||||||
|
return errorNotification(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<td>
|
<div class="w-full font-bold grid grid-cols-1 lg:grid-cols-4 gap-2 pb-2">
|
||||||
<input
|
<div class="flex flex-col">
|
||||||
id={isNewSecret ? 'secretName' : 'secretNameNew'}
|
{#if (index === 0 && !isNewSecret) || length === 0}
|
||||||
bind:value={name}
|
<label for="name" class="pb-2 uppercase">name</label>
|
||||||
required
|
{/if}
|
||||||
placeholder="EXAMPLE_VARIABLE"
|
|
||||||
readonly={!isNewSecret}
|
<input
|
||||||
class:bg-transparent={!isNewSecret}
|
id={isNewSecret ? 'secretName' : 'secretNameNew'}
|
||||||
class:cursor-not-allowed={!isNewSecret}
|
bind:value={name}
|
||||||
/>
|
required
|
||||||
</td>
|
placeholder="EXAMPLE_VARIABLE"
|
||||||
<td>
|
readonly={!isNewSecret}
|
||||||
<CopyPasswordField
|
class=" w-full"
|
||||||
id={isNewSecret ? 'secretValue' : 'secretValueNew'}
|
class:bg-coolblack={!isNewSecret}
|
||||||
name={isNewSecret ? 'secretValue' : 'secretValueNew'}
|
class:border={!isNewSecret}
|
||||||
isPasswordField={true}
|
class:border-dashed={!isNewSecret}
|
||||||
bind:value
|
class:border-coolgray-300={!isNewSecret}
|
||||||
required
|
class:cursor-not-allowed={!isNewSecret}
|
||||||
placeholder="J$#@UIO%HO#$U%H"
|
/>
|
||||||
/>
|
</div>
|
||||||
</td>
|
<div class="flex flex-col">
|
||||||
<td class="text-center">
|
{#if (index === 0 && !isNewSecret) || length === 0}
|
||||||
<button
|
<label for="value" class="pb-2 uppercase">value</label>
|
||||||
on:click={setSecretValue}
|
{/if}
|
||||||
aria-pressed="false"
|
|
||||||
class="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out"
|
<CopyPasswordField
|
||||||
class:bg-green-600={isBuildSecret}
|
id={isNewSecret ? 'secretValue' : 'secretValueNew'}
|
||||||
class:bg-stone-700={!isBuildSecret}
|
name={isNewSecret ? 'secretValue' : 'secretValueNew'}
|
||||||
class:opacity-50={isPRMRSecret}
|
isPasswordField={true}
|
||||||
class:cursor-not-allowed={isPRMRSecret}
|
bind:value
|
||||||
class:cursor-pointer={!isPRMRSecret}
|
placeholder="J$#@UIO%HO#$U%H"
|
||||||
>
|
/>
|
||||||
<span class="sr-only">{$t('application.secrets.use_isbuildsecret')}</span>
|
</div>
|
||||||
<span
|
<div class="flex lg:flex-col flex-row justify-start items-center pt-3 lg:pt-0">
|
||||||
class="pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow transition duration-200 ease-in-out"
|
{#if (index === 0 && !isNewSecret) || length === 0}
|
||||||
class:translate-x-5={isBuildSecret}
|
<label for="name" class="pb-2 uppercase lg:block hidden">Need during buildtime?</label>
|
||||||
class:translate-x-0={!isBuildSecret}
|
{/if}
|
||||||
>
|
<label for="name" class="pb-2 uppercase lg:hidden block">Need during buildtime?</label>
|
||||||
<span
|
|
||||||
class=" absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
|
<div class="flex justify-center h-full items-center pt-0 lg:pt-0 pl-4 lg:pl-0">
|
||||||
class:opacity-0={isBuildSecret}
|
<button
|
||||||
class:opacity-100={!isBuildSecret}
|
on:click={() => updateSecret({ changeIsBuildSecret: true })}
|
||||||
aria-hidden="true"
|
aria-pressed="false"
|
||||||
|
class="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out "
|
||||||
|
class:bg-green-600={isBuildSecret}
|
||||||
|
class:bg-stone-700={!isBuildSecret}
|
||||||
>
|
>
|
||||||
<svg class="h-3 w-3 bg-white text-red-600" fill="none" viewBox="0 0 12 12">
|
<span class="sr-only">{$t('application.secrets.use_isbuildsecret')}</span>
|
||||||
<path
|
<span
|
||||||
d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
|
class="pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow transition duration-200 ease-in-out"
|
||||||
stroke="currentColor"
|
class:translate-x-5={isBuildSecret}
|
||||||
stroke-width="2"
|
class:translate-x-0={!isBuildSecret}
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-100 ease-out"
|
|
||||||
aria-hidden="true"
|
|
||||||
class:opacity-100={isBuildSecret}
|
|
||||||
class:opacity-0={!isBuildSecret}
|
|
||||||
>
|
|
||||||
<svg class="h-3 w-3 bg-white text-green-600" fill="currentColor" viewBox="0 0 12 12">
|
|
||||||
<path
|
|
||||||
d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{#if isNewSecret}
|
|
||||||
<div class="flex items-center justify-center">
|
|
||||||
<button class="btn bg-applications btn-sm" on:click={() => createSecret(true)}
|
|
||||||
>{$t('forms.add')}</button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="flex flex-row justify-center space-x-2">
|
|
||||||
<div class="flex items-center justify-center">
|
|
||||||
<button class="btn bg-application btn-sm" on:click={() => createSecret(false)}
|
|
||||||
>{$t('forms.set')}</button
|
|
||||||
>
|
>
|
||||||
</div>
|
<span
|
||||||
{#if !isPRMRSecret}
|
class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
|
||||||
<div class="flex justify-center items-end">
|
class:opacity-0={isBuildSecret}
|
||||||
<button class="btn btn-sm bg-red-600 hover:bg-red-500" on:click={removeSecret}
|
class:opacity-100={!isBuildSecret}
|
||||||
>{$t('forms.remove')}</button
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
|
<svg class="h-3 w-3 bg-white text-red-600" fill="none" viewBox="0 0 12 12">
|
||||||
|
<path
|
||||||
|
d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-100 ease-out"
|
||||||
|
aria-hidden="true"
|
||||||
|
class:opacity-100={isBuildSecret}
|
||||||
|
class:opacity-0={!isBuildSecret}
|
||||||
|
>
|
||||||
|
<svg class="h-3 w-3 bg-white text-green-600" fill="currentColor" viewBox="0 0 12 12">
|
||||||
|
<path
|
||||||
|
d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row lg:flex-col lg:items-center items-start">
|
||||||
|
{#if (index === 0 && !isNewSecret) || length === 0}
|
||||||
|
<label for="name" class="pb-2 uppercase lg:block hidden">Actions</label>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="flex justify-center h-full items-center pt-3">
|
||||||
|
{#if isNewSecret}
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<button class="btn btn-sm btn-primary" on:click={addNewSecret}>Add</button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-row justify-center space-x-2">
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<button class="btn btn-sm btn-primary" on:click={() => updateSecret()}>Set</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center items-end">
|
||||||
|
<button class="btn btn-sm btn-error" on:click={removeSecret}>Remove</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</div>
|
||||||
</td>
|
</div>
|
||||||
|
|||||||
@@ -59,32 +59,36 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<td>
|
<div class="w-full font-bold grid gap-2">
|
||||||
<input
|
<div class="flex flex-col pb-2">
|
||||||
bind:value={storage.path}
|
|
||||||
required
|
<div class="flex flex-col lg:flex-row lg:space-y-0 space-y-2">
|
||||||
placeholder="eg: /sqlite.db"
|
<input
|
||||||
/>
|
class="w-full lg:w-64"
|
||||||
</td>
|
bind:value={storage.path}
|
||||||
<td>
|
required
|
||||||
{#if isNew}
|
placeholder="eg: /sqlite.db"
|
||||||
<div class="flex items-center justify-center">
|
/>
|
||||||
<button class="btn btn-sm bg-applications" on:click={() => saveStorage(true)}
|
{#if isNew}
|
||||||
>{$t('forms.add')}</button
|
<div class="flex items-center justify-center w-full lg:w-64">
|
||||||
>
|
<button class="btn btn-sm btn-primary" on:click={() => saveStorage(true)}
|
||||||
|
>{$t('forms.add')}</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-row items-center justify-center space-x-2 w-full lg:w-64">
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<button class="btn btn-sm btn-primary" on:click={() => saveStorage(false)}
|
||||||
|
>{$t('forms.set')}</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<button class="btn btn-sm btn-error" on:click={removeStorage}
|
||||||
|
>{$t('forms.remove')}</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
</div>
|
||||||
<div class="flex flex-row justify-center space-x-2">
|
</div>
|
||||||
<div class="flex items-center justify-center">
|
|
||||||
<button class="btn btn-sm bg-applications" on:click={() => saveStorage(false)}
|
|
||||||
>{$t('forms.set')}</button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-center items-end">
|
|
||||||
<button class="btn btn-sm bg-red-600 hover:bg-red-500" on:click={removeStorage}
|
|
||||||
>{$t('forms.remove')}</button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</td>
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
if (!application || Object.entries(application).length === 0) {
|
if (!application || Object.entries(application).length === 0) {
|
||||||
return {
|
return {
|
||||||
status: 302,
|
status: 302,
|
||||||
redirect: '/applications'
|
redirect: '/'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const configurationPhase = checkConfiguration(application);
|
const configurationPhase = checkConfiguration(application);
|
||||||
@@ -55,7 +55,6 @@
|
|||||||
export let application: any;
|
export let application: any;
|
||||||
export let settings: any;
|
export let settings: any;
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
|
|
||||||
import { del, get, post } from '$lib/api';
|
import { del, get, post } from '$lib/api';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
@@ -67,10 +66,12 @@
|
|||||||
setLocation,
|
setLocation,
|
||||||
addToast,
|
addToast,
|
||||||
isDeploymentEnabled,
|
isDeploymentEnabled,
|
||||||
checkIfDeploymentEnabledApplications
|
checkIfDeploymentEnabledApplications,
|
||||||
|
selectedBuildId
|
||||||
} from '$lib/store';
|
} from '$lib/store';
|
||||||
import { errorNotification, handlerNotFoundLoad } from '$lib/common';
|
import { errorNotification, handlerNotFoundLoad } from '$lib/common';
|
||||||
import Tooltip from '$lib/components/Tooltip.svelte';
|
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||||
|
import Menu from './_Menu.svelte';
|
||||||
|
|
||||||
let statusInterval: any;
|
let statusInterval: any;
|
||||||
let forceDelete = false;
|
let forceDelete = false;
|
||||||
@@ -89,35 +90,15 @@
|
|||||||
message: $t('application.deployment_queued'),
|
message: $t('application.deployment_queued'),
|
||||||
type: 'success'
|
type: 'success'
|
||||||
});
|
});
|
||||||
if ($page.url.pathname.startsWith(`/applications/${id}/logs/build`)) {
|
$selectedBuildId = buildId;
|
||||||
return window.location.assign(`/applications/${id}/logs/build?buildId=${buildId}`);
|
return await goto(`/applications/${id}/logs/build?buildId=${buildId}`, {
|
||||||
} else {
|
replaceState: true
|
||||||
return await goto(`/applications/${id}/logs/build?buildId=${buildId}`, {
|
});
|
||||||
replaceState: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorNotification(error);
|
return errorNotification(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteApplication(name: string, force: boolean) {
|
|
||||||
const sure = confirm($t('application.confirm_to_delete', { name }));
|
|
||||||
if (sure) {
|
|
||||||
$status.application.initialLoading = true;
|
|
||||||
try {
|
|
||||||
await del(`/applications/${id}`, { id, force });
|
|
||||||
return await goto(`/applications`);
|
|
||||||
} catch (error) {
|
|
||||||
if (error.message.startsWith(`Command failed: SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid`)) {
|
|
||||||
forceDelete = true;
|
|
||||||
}
|
|
||||||
return errorNotification(error);
|
|
||||||
} finally {
|
|
||||||
$status.application.initialLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function restartApplication() {
|
async function restartApplication() {
|
||||||
try {
|
try {
|
||||||
$status.application.initialLoading = true;
|
$status.application.initialLoading = true;
|
||||||
@@ -154,6 +135,7 @@
|
|||||||
const data = await get(`/applications/${id}/status`);
|
const data = await get(`/applications/${id}/status`);
|
||||||
$status.application.isRunning = data.isRunning;
|
$status.application.isRunning = data.isRunning;
|
||||||
$status.application.isExited = data.isExited;
|
$status.application.isExited = data.isExited;
|
||||||
|
$status.application.isRestarting = data.isRestarting;
|
||||||
$status.application.loading = false;
|
$status.application.loading = false;
|
||||||
$status.application.initialLoading = false;
|
$status.application.initialLoading = false;
|
||||||
}
|
}
|
||||||
@@ -162,6 +144,7 @@
|
|||||||
$status.application.initialLoading = true;
|
$status.application.initialLoading = true;
|
||||||
$status.application.isRunning = false;
|
$status.application.isRunning = false;
|
||||||
$status.application.isExited = false;
|
$status.application.isExited = false;
|
||||||
|
$status.application.isRestarting = false;
|
||||||
$status.application.loading = false;
|
$status.application.loading = false;
|
||||||
$location = null;
|
$location = null;
|
||||||
$isDeploymentEnabled = false;
|
$isDeploymentEnabled = false;
|
||||||
@@ -171,6 +154,7 @@
|
|||||||
setLocation(application, settings);
|
setLocation(application, settings);
|
||||||
$status.application.isRunning = false;
|
$status.application.isRunning = false;
|
||||||
$status.application.isExited = false;
|
$status.application.isExited = false;
|
||||||
|
$status.application.isRestarting = false;
|
||||||
$status.application.loading = false;
|
$status.application.loading = false;
|
||||||
if (
|
if (
|
||||||
application.gitSourceId &&
|
application.gitSourceId &&
|
||||||
@@ -187,139 +171,136 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav class="nav-side">
|
<div class="mx-auto max-w-screen-2xl px-6 grid grid-cols-1 lg:grid-cols-2">
|
||||||
{#if $location}
|
<nav class="header flex flex-row order-2 lg:order-1 px-0 lg:px-4">
|
||||||
<a
|
<div class="title lg:pb-10">
|
||||||
id="open"
|
{#if $page.url.pathname === `/applications/${id}/configuration/source`}
|
||||||
href={$location}
|
Select a Source
|
||||||
target="_blank"
|
{:else if $page.url.pathname === `/applications/${id}/configuration/destination`}
|
||||||
class="icons flex items-center bg-transparent text-sm"
|
Select a Destination
|
||||||
><svg
|
{:else if $page.url.pathname === `/applications/${id}/configuration/repository`}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
Select a Repository
|
||||||
class="h-6 w-6"
|
{:else if $page.url.pathname === `/applications/${id}/configuration/buildpack`}
|
||||||
viewBox="0 0 24 24"
|
Select a Build Pack
|
||||||
stroke-width="1.5"
|
{:else}
|
||||||
stroke="currentColor"
|
<div class="flex justify-center items-center space-x-2">
|
||||||
fill="none"
|
<div>Configurations</div>
|
||||||
stroke-linecap="round"
|
<div
|
||||||
stroke-linejoin="round"
|
class="badge rounded uppercase"
|
||||||
|
class:text-green-500={$status.application.isRunning}
|
||||||
|
class:text-red-500={!$status.application.isRunning}
|
||||||
|
>
|
||||||
|
{$status.application.isRunning ? 'Running' : 'Stopped'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div
|
||||||
|
class="pt-4 flex flex-row items-start justify-center lg:justify-end space-x-2 order-1 lg:order-2"
|
||||||
|
>
|
||||||
|
{#if $status.application.isExited || $status.application.isRestarting}
|
||||||
|
<a
|
||||||
|
id="applicationerror"
|
||||||
|
href={$isDeploymentEnabled ? `/applications/${id}/logs` : null}
|
||||||
|
class="icons bg-transparent text-sm text-error"
|
||||||
|
sveltekit:prefetch
|
||||||
>
|
>
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
<svg
|
||||||
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<line x1="10" y1="14" x2="20" y2="4" />
|
class="w-6 h-6"
|
||||||
<polyline points="15 4 20 4 20 9" />
|
viewBox="0 0 24 24"
|
||||||
</svg></a
|
stroke-width="1.5"
|
||||||
>
|
stroke="currentcolor"
|
||||||
<Tooltip triggeredBy="#open">Open</Tooltip>
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
<div class="border border-coolgray-500 h-8" />
|
stroke-linejoin="round"
|
||||||
{/if}
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
{#if $status.application.isExited}
|
<path
|
||||||
<a
|
d="M8.7 3h6.6c.3 0 .5 .1 .7 .3l4.7 4.7c.2 .2 .3 .4 .3 .7v6.6c0 .3 -.1 .5 -.3 .7l-4.7 4.7c-.2 .2 -.4 .3 -.7 .3h-6.6c-.3 0 -.5 -.1 -.7 -.3l-4.7 -4.7c-.2 -.2 -.3 -.4 -.3 -.7v-6.6c0 -.3 .1 -.5 .3 -.7l4.7 -4.7c.2 -.2 .4 -.3 .7 -.3z"
|
||||||
id="applicationerror"
|
/>
|
||||||
href={$isDeploymentEnabled ? `/applications/${id}/logs` : null}
|
<line x1="12" y1="8" x2="12" y2="12" />
|
||||||
class="icons bg-transparent text-sm flex items-center text-error"
|
<line x1="12" y1="16" x2="12.01" y2="16" />
|
||||||
sveltekit:prefetch
|
</svg>
|
||||||
>
|
</a>
|
||||||
<svg
|
<Tooltip triggeredBy="#applicationerror">Application exited with an error!</Tooltip>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
{/if}
|
||||||
class="w-6 h-6"
|
{#if $status.application.initialLoading}
|
||||||
viewBox="0 0 24 24"
|
<button class="icons animate-spin bg-transparent duration-500 ease-in-out">
|
||||||
stroke-width="1.5"
|
<svg
|
||||||
stroke="currentcolor"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
class="h-6 w-6"
|
||||||
stroke-linecap="round"
|
viewBox="0 0 24 24"
|
||||||
stroke-linejoin="round"
|
stroke-width="1.5"
|
||||||
>
|
stroke="currentColor"
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
fill="none"
|
||||||
<path
|
stroke-linecap="round"
|
||||||
d="M8.7 3h6.6c.3 0 .5 .1 .7 .3l4.7 4.7c.2 .2 .3 .4 .3 .7v6.6c0 .3 -.1 .5 -.3 .7l-4.7 4.7c-.2 .2 -.4 .3 -.7 .3h-6.6c-.3 0 -.5 -.1 -.7 -.3l-4.7 -4.7c-.2 -.2 -.3 -.4 -.3 -.7v-6.6c0 -.3 .1 -.5 .3 -.7l4.7 -4.7c.2 -.2 .4 -.3 .7 -.3z"
|
stroke-linejoin="round"
|
||||||
/>
|
>
|
||||||
<line x1="12" y1="8" x2="12" y2="12" />
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
<line x1="12" y1="16" x2="12.01" y2="16" />
|
<path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5" />
|
||||||
</svg>
|
<line x1="5.63" y1="7.16" x2="5.63" y2="7.17" />
|
||||||
</a>
|
<line x1="4.06" y1="11" x2="4.06" y2="11.01" />
|
||||||
<Tooltip triggeredBy="#applicationerror">Application exited with an error!</Tooltip>
|
<line x1="4.63" y1="15.1" x2="4.63" y2="15.11" />
|
||||||
{/if}
|
<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
|
||||||
{#if $status.application.initialLoading}
|
<line x1="11" y1="19.94" x2="11" y2="19.95" />
|
||||||
<button
|
</svg>
|
||||||
class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out hover:bg-transparent"
|
</button>
|
||||||
>
|
{:else if $status.application.isRunning}
|
||||||
<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="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5" />
|
|
||||||
<line x1="5.63" y1="7.16" x2="5.63" y2="7.17" />
|
|
||||||
<line x1="4.06" y1="11" x2="4.06" y2="11.01" />
|
|
||||||
<line x1="4.63" y1="15.1" x2="4.63" y2="15.11" />
|
|
||||||
<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
|
|
||||||
<line x1="11" y1="19.94" x2="11" y2="19.95" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
{:else if $status.application.isRunning}
|
|
||||||
<button
|
|
||||||
id="stop"
|
|
||||||
on:click={stopApplication}
|
|
||||||
type="submit"
|
|
||||||
disabled={!$isDeploymentEnabled}
|
|
||||||
class="icons bg-transparent text-sm flex items-center space-x-2 text-error"
|
|
||||||
>
|
|
||||||
<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" />
|
|
||||||
<rect x="6" y="5" width="4" height="14" rx="1" />
|
|
||||||
<rect x="14" y="5" width="4" height="14" rx="1" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<Tooltip triggeredBy="#stop">Stop</Tooltip>
|
|
||||||
|
|
||||||
<button
|
|
||||||
id="restart"
|
|
||||||
on:click={restartApplication}
|
|
||||||
type="submit"
|
|
||||||
disabled={!$isDeploymentEnabled}
|
|
||||||
class="icons bg-transparent text-sm flex items-center space-x-2"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="w-6 h-6"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
fill="none"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
>
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
||||||
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" />
|
|
||||||
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<Tooltip triggeredBy="#restart">Restart (useful to change secrets)</Tooltip>
|
|
||||||
|
|
||||||
<form on:submit|preventDefault={() => handleDeploySubmit(true)}>
|
|
||||||
<button
|
<button
|
||||||
id="forceredeploy"
|
id="stop"
|
||||||
|
on:click={stopApplication}
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!$isDeploymentEnabled}
|
disabled={!$isDeploymentEnabled}
|
||||||
class="icons bg-transparent text-sm flex items-center space-x-2"
|
class="icons bg-transparent text-error"
|
||||||
|
>
|
||||||
|
<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" />
|
||||||
|
<rect x="6" y="5" width="4" height="14" rx="1" />
|
||||||
|
<rect x="14" y="5" width="4" height="14" rx="1" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<Tooltip triggeredBy="#stop">Stop</Tooltip>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="restart"
|
||||||
|
on:click={restartApplication}
|
||||||
|
type="submit"
|
||||||
|
disabled={!$isDeploymentEnabled}
|
||||||
|
class="icons bg-transparent"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="w-6 h-6"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" />
|
||||||
|
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<Tooltip triggeredBy="#restart">Restart (useful to change secrets)</Tooltip>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="forceredeploy"
|
||||||
|
disabled={!$isDeploymentEnabled}
|
||||||
|
class="icons bg-transparent "
|
||||||
|
on:click={() => handleDeploySubmit(true)}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -338,19 +319,17 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<Tooltip triggeredBy="#forceredeploy">Force redeploy (without cache)</Tooltip>
|
<Tooltip triggeredBy="#forceredeploy">Force Redeploy (without cache)</Tooltip>
|
||||||
</form>
|
{:else}
|
||||||
{:else}
|
{#if $isDeploymentEnabled}
|
||||||
<form on:submit|preventDefault={() => handleDeploySubmit(false)}>
|
|
||||||
<button
|
<button
|
||||||
id="deploy"
|
class="icons flex items-center font-bold"
|
||||||
type="submit"
|
|
||||||
disabled={!$isDeploymentEnabled}
|
disabled={!$isDeploymentEnabled}
|
||||||
class="icons bg-transparent text-sm flex items-center space-x-2 text-success"
|
on:click={() => handleDeploySubmit(false)}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="w-6 h-6"
|
class="w-6 h-6 mr-2 text-green-500"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -361,119 +340,16 @@
|
|||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
<path d="M7 4v16l13 -8z" />
|
<path d="M7 4v16l13 -8z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
Deploy
|
||||||
</button>
|
</button>
|
||||||
<Tooltip triggeredBy="#deploy">Deploy</Tooltip>
|
{/if}
|
||||||
</form>
|
{/if}
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="border border-coolgray-500 h-8" />
|
{#if $location && $status.application.isRunning}
|
||||||
<a
|
<a id="openApplication" href={$location} target="_blank" class="icons bg-transparent "
|
||||||
href={$isDeploymentEnabled ? `/applications/${id}` : null}
|
><svg
|
||||||
sveltekit:prefetch
|
|
||||||
class="hover:text-yellow-500 rounded"
|
|
||||||
class:text-yellow-500={$page.url.pathname === `/applications/${id}`}
|
|
||||||
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
disabled={!$isDeploymentEnabled}
|
|
||||||
id="configurations"
|
|
||||||
class="icons 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" />
|
|
||||||
<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
|
|
||||||
>
|
|
||||||
|
|
||||||
<Tooltip triggeredBy="#configurations">Configurations</Tooltip>
|
|
||||||
<a
|
|
||||||
href={$isDeploymentEnabled ? `/applications/${id}/secrets` : null}
|
|
||||||
sveltekit:prefetch
|
|
||||||
class="hover:text-pink-500 rounded"
|
|
||||||
class:text-pink-500={$page.url.pathname === `/applications/${id}/secrets`}
|
|
||||||
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/secrets`}
|
|
||||||
>
|
|
||||||
<button id="secrets" disabled={!$isDeploymentEnabled} class="icons bg-transparent text-sm">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="w-6 h-6"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
fill="none"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
>
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
||||||
<path
|
|
||||||
d="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3"
|
|
||||||
/>
|
|
||||||
<circle cx="12" cy="11" r="1" />
|
|
||||||
<line x1="12" y1="12" x2="12" y2="14.5" />
|
|
||||||
</svg></button
|
|
||||||
></a
|
|
||||||
>
|
|
||||||
<Tooltip triggeredBy="#secrets">Secrets</Tooltip>
|
|
||||||
<a
|
|
||||||
href={$isDeploymentEnabled ? `/applications/${id}/storages` : null}
|
|
||||||
sveltekit:prefetch
|
|
||||||
class="hover:text-pink-500 rounded"
|
|
||||||
class:text-pink-500={$page.url.pathname === `/applications/${id}/storages`}
|
|
||||||
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/storages`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
id="persistentstorages"
|
|
||||||
disabled={!$isDeploymentEnabled}
|
|
||||||
class="icons bg-transparent text-sm"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="w-6 h-6"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
fill="none"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
>
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
||||||
<ellipse cx="12" cy="6" rx="8" ry="3" />
|
|
||||||
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
|
|
||||||
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
|
|
||||||
</svg>
|
|
||||||
</button></a
|
|
||||||
>
|
|
||||||
<Tooltip triggeredBy="#persistentstorages">Persistent Storages</Tooltip>
|
|
||||||
{#if !application.settings.isBot}
|
|
||||||
<a
|
|
||||||
href={$isDeploymentEnabled ? `/applications/${id}/previews` : null}
|
|
||||||
sveltekit:prefetch
|
|
||||||
class="hover:text-orange-500 rounded"
|
|
||||||
class:text-orange-500={$page.url.pathname === `/applications/${id}/previews`}
|
|
||||||
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/previews`}
|
|
||||||
>
|
|
||||||
<button id="previews" disabled={!$isDeploymentEnabled} class="icons bg-transparent text-sm">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="w-6 h-6"
|
class="h-6 w-6"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -482,107 +358,25 @@
|
|||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
>
|
>
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
<circle cx="7" cy="18" r="2" />
|
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
|
||||||
<circle cx="7" cy="6" r="2" />
|
<line x1="10" y1="14" x2="20" y2="4" />
|
||||||
<circle cx="17" cy="12" r="2" />
|
<polyline points="15 4 20 4 20 9" />
|
||||||
<line x1="7" y1="8" x2="7" y2="16" />
|
</svg></a
|
||||||
<path d="M7 8a4 4 0 0 0 4 4h4" />
|
|
||||||
</svg></button
|
|
||||||
></a
|
|
||||||
>
|
|
||||||
<Tooltip triggeredBy="#previews">Previews</Tooltip>
|
|
||||||
{/if}
|
|
||||||
<div class="border border-coolgray-500 h-8" />
|
|
||||||
<a
|
|
||||||
href={$isDeploymentEnabled && $status.application.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
|
|
||||||
id="applicationlogs"
|
|
||||||
disabled={!$isDeploymentEnabled || !$status.application.isRunning}
|
|
||||||
class="icons 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" />
|
<Tooltip triggeredBy="#openApplication">Open Application</Tooltip>
|
||||||
<path d="M3 19a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
|
{/if}
|
||||||
<path d="M3 6a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
|
</div>
|
||||||
<line x1="3" y1="6" x2="3" y2="19" />
|
</div>
|
||||||
<line x1="12" y1="6" x2="12" y2="19" />
|
<div
|
||||||
<line x1="21" y1="6" x2="21" y2="19" />
|
class="mx-auto max-w-screen-2xl px-0 lg:px-2 grid grid-cols-1"
|
||||||
</svg>
|
class:lg:grid-cols-4={!$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
|
||||||
</button></a
|
>
|
||||||
>
|
{#if !$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
|
||||||
<Tooltip triggeredBy="#applicationlogs">Application Logs</Tooltip>
|
<nav class="header flex flex-col lg:pt-0 ">
|
||||||
<a
|
<Menu {application} />
|
||||||
href={$isDeploymentEnabled ? `/applications/${id}/logs/build` : null}
|
</nav>
|
||||||
sveltekit:prefetch
|
|
||||||
class="hover:text-red-500 rounded"
|
|
||||||
class:text-red-500={$page.url.pathname === `/applications/${id}/logs/build`}
|
|
||||||
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs/build`}
|
|
||||||
>
|
|
||||||
<button id="buildlogs" disabled={!$isDeploymentEnabled} class="icons 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" />
|
|
||||||
<circle cx="19" cy="13" r="2" />
|
|
||||||
<circle cx="4" cy="17" r="2" />
|
|
||||||
<circle cx="13" cy="17" r="2" />
|
|
||||||
<line x1="13" y1="19" x2="4" y2="19" />
|
|
||||||
<line x1="4" y1="15" x2="13" y2="15" />
|
|
||||||
<path d="M8 12v-5h2a3 3 0 0 1 3 3v5" />
|
|
||||||
<path d="M5 15v-2a1 1 0 0 1 1 -1h7" />
|
|
||||||
<path d="M19 11v-7l-6 7" />
|
|
||||||
</svg>
|
|
||||||
</button></a
|
|
||||||
>
|
|
||||||
<Tooltip triggeredBy="#buildlogs">Build Logs</Tooltip>
|
|
||||||
<div class="border border-coolgray-500 h-8" />
|
|
||||||
|
|
||||||
{#if forceDelete}
|
|
||||||
<button
|
|
||||||
id="forcedelete"
|
|
||||||
on:click={() => deleteApplication(application.name, true)}
|
|
||||||
type="submit"
|
|
||||||
disabled={!$appSession.isAdmin}
|
|
||||||
class:bg-red-600={$appSession.isAdmin}
|
|
||||||
class:hover:bg-red-500={$appSession.isAdmin}
|
|
||||||
class="icons bg-transparent text-sm"
|
|
||||||
>
|
|
||||||
Force Delete
|
|
||||||
</button>
|
|
||||||
<Tooltip triggeredBy="#forcedelete">Force Delete</Tooltip>
|
|
||||||
{:else}
|
|
||||||
<button
|
|
||||||
id="delete"
|
|
||||||
on:click={() => deleteApplication(application.name, false)}
|
|
||||||
type="submit"
|
|
||||||
disabled={!$appSession.isAdmin}
|
|
||||||
class:hover:text-red-500={$appSession.isAdmin}
|
|
||||||
class="icons bg-transparent text-sm"
|
|
||||||
>
|
|
||||||
<DeleteIcon />
|
|
||||||
</button>
|
|
||||||
<Tooltip triggeredBy="#delete">Delete</Tooltip>
|
|
||||||
{/if}
|
{/if}
|
||||||
</nav>
|
<div class="pt-0 col-span-0 lg:col-span-3 pb-24">
|
||||||
<slot />
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -40,9 +40,13 @@
|
|||||||
<form on:submit|preventDefault={() => handleSubmit(buildPack.name)}>
|
<form on:submit|preventDefault={() => handleSubmit(buildPack.name)}>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="box-selection relative flex text-xl font-bold {buildPack.hoverColor} {foundConfig?.name ===
|
class="box-selection relative flex flex-col items-center text-xl font-bold {buildPack.hoverColor} {foundConfig?.name ===
|
||||||
buildPack.name && buildPack.color}"
|
buildPack.name && buildPack.color}"
|
||||||
><span>{buildPack.fancyName}</span>
|
>
|
||||||
|
<div>{buildPack.fancyName}</div>
|
||||||
|
{#if buildPack.base}
|
||||||
|
<div class="text-xs font-mono">{buildPack.base}</div>
|
||||||
|
{/if}
|
||||||
{#if !scanning && foundConfig?.name === buildPack.name}
|
{#if !scanning && foundConfig?.name === buildPack.name}
|
||||||
<span class="absolute bottom-0 pb-2 text-xs"
|
<span class="absolute bottom-0 pb-2 text-xs"
|
||||||
>{$t('application.configuration.buildpack.choose_this_one')}</span
|
>{$t('application.configuration.buildpack.choose_this_one')}</span
|
||||||
|
|||||||
@@ -143,7 +143,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if repositories.length === 0 && loading.repositories === false}
|
{#if repositories.length === 0 && loading.repositories === false}
|
||||||
<div class="flex-col text-center">
|
<div class="flex-col text-center">
|
||||||
<div class="pb-4">{$t('application.configuration.no_repositories_configured')}</div>
|
<div class="pb-4">{$t('application.configuration.no_repositories_configured')}</div>
|
||||||
@@ -152,10 +151,9 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<form on:submit|preventDefault={handleSubmit} class="flex flex-col justify-center text-center">
|
<form on:submit|preventDefault={handleSubmit} class="px-10">
|
||||||
<div class="flex-col space-y-3 md:space-y-0 space-x-1">
|
<div class="flex lg:flex-row flex-col lg:space-y-0 space-y-2 space-x-0 lg:space-x-2 items-center">
|
||||||
<div class="flex-row md:flex gap-4">
|
<div class="custom-select-wrapper w-1/2"><label for="repository" class="pb-1">Repository</label>
|
||||||
<div class="custom-select-wrapper">
|
|
||||||
<Select
|
<Select
|
||||||
placeholder={loading.repositories
|
placeholder={loading.repositories
|
||||||
? $t('application.configuration.loading_repositories')
|
? $t('application.configuration.loading_repositories')
|
||||||
@@ -170,7 +168,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<input class="hidden" bind:value={selected.projectId} name="projectId" />
|
<input class="hidden" bind:value={selected.projectId} name="projectId" />
|
||||||
<div class="custom-select-wrapper">
|
<div class="custom-select-wrapper w-1/2"><label for="repository" class="pb-1">Branch</label>
|
||||||
<Select
|
<Select
|
||||||
placeholder={loading.branches
|
placeholder={loading.branches
|
||||||
? $t('application.configuration.loading_branches')
|
? $t('application.configuration.loading_branches')
|
||||||
@@ -185,9 +183,7 @@
|
|||||||
isDisabled={loading.branches || !selected.repository}
|
isDisabled={loading.branches || !selected.repository}
|
||||||
isClearable={false}
|
isClearable={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div></div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pt-5 flex-col flex justify-center items-center space-y-4">
|
<div class="pt-5 flex-col flex justify-center items-center space-y-4">
|
||||||
<button
|
<button
|
||||||
class="btn btn-wide"
|
class="btn btn-wide"
|
||||||
|
|||||||
@@ -413,7 +413,7 @@
|
|||||||
>{loading.save ? $t('forms.saving') : $t('forms.save')}</button
|
>{loading.save ? $t('forms.saving') : $t('forms.save')}</button
|
||||||
>
|
>
|
||||||
{#if tryAgain}
|
{#if tryAgain}
|
||||||
<div>
|
<div class="p-5">
|
||||||
An error occured during authenticating with GitLab. Please check your GitLab Source
|
An error occured during authenticating with GitLab. Please check your GitLab Source
|
||||||
configuration <a href={`/sources/${application.gitSource.id}`}>here.</a>
|
configuration <a href={`/sources/${application.gitSource.id}`}>here.</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
};
|
};
|
||||||
async function loadBranches() {
|
async function loadBranches() {
|
||||||
try {
|
try {
|
||||||
|
if (!publicRepositoryLink) return
|
||||||
loading.branches = true;
|
loading.branches = true;
|
||||||
publicRepositoryLink = publicRepositoryLink.trim();
|
publicRepositoryLink = publicRepositoryLink.trim();
|
||||||
const protocol = publicRepositoryLink.split(':')[0];
|
const protocol = publicRepositoryLink.split(':')[0];
|
||||||
@@ -156,40 +157,36 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-auto max-w-5xl">
|
<div class="mx-auto max-w-screen-2xl">
|
||||||
<div class="grid grid-flow-row gap-2 px-10">
|
<form class="flex flex-col" on:submit|preventDefault={loadBranches}>
|
||||||
<div class="flex">
|
<div class="flex flex-col space-y-2 w-full">
|
||||||
<form class="flex" on:submit|preventDefault={loadBranches}>
|
<div class="flex flex-row space-x-2"><input
|
||||||
<div class="space-y-4">
|
class="w-full"
|
||||||
<input
|
placeholder="eg: https://github.com/coollabsio/nodejs-example/tree/main"
|
||||||
placeholder="eg: https://github.com/coollabsio/nodejs-example/tree/main"
|
bind:value={publicRepositoryLink}
|
||||||
bind:value={publicRepositoryLink}
|
/>
|
||||||
/>
|
<button class="btn bg-orange-600" class:loading={loading.branches} type="submit">
|
||||||
{#if branchSelectOptions.length > 0}
|
Load Repository
|
||||||
<div class="custom-select-wrapper">
|
</button>
|
||||||
<Select
|
</div>
|
||||||
placeholder={loading.branches
|
|
||||||
? $t('application.configuration.loading_branches')
|
<div class="custom-select-wrapper">
|
||||||
: !publicRepositoryLink
|
<Select
|
||||||
? $t('application.configuration.select_a_repository_first')
|
class="w-full"
|
||||||
: $t('application.configuration.select_a_branch')}
|
placeholder={loading.branches
|
||||||
isWaiting={loading.branches}
|
? $t('application.configuration.loading_branches')
|
||||||
showIndicator={!!publicRepositoryLink && !loading.branches}
|
: branchSelectOptions.length ===0
|
||||||
id="branches"
|
? 'Please type a repository link first.'
|
||||||
on:select={saveRepository}
|
: $t('application.configuration.select_a_branch')}
|
||||||
items={branchSelectOptions}
|
isWaiting={loading.branches}
|
||||||
isDisabled={loading.branches || !!!publicRepositoryLink}
|
showIndicator={!!publicRepositoryLink && !loading.branches}
|
||||||
isClearable={false}
|
id="branches"
|
||||||
/>
|
on:select={saveRepository}
|
||||||
</div>
|
items={branchSelectOptions}
|
||||||
{/if}
|
isDisabled={loading.branches || !ownerName}
|
||||||
</div>
|
isClearable={false}
|
||||||
|
/>
|
||||||
<button class="btn mx-4 bg-orange-600" class:loading={loading.branches} type="submit"
|
</div>
|
||||||
>Load Repository</button
|
|
||||||
>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -254,12 +254,6 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex space-x-1 p-6 font-bold">
|
|
||||||
<div class="mr-4 text-2xl tracking-tight">
|
|
||||||
{$t('application.configuration.configure_build_pack')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if scanning}
|
{#if scanning}
|
||||||
<div class="flex justify-center space-x-1 p-6 font-bold">
|
<div class="flex justify-center space-x-1 p-6 font-bold">
|
||||||
<div class="text-xl tracking-tight">
|
<div class="text-xl tracking-tight">
|
||||||
@@ -267,18 +261,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="max-w-5xl mx-auto ">
|
<div class="max-w-screen-2xl mx-auto px-10">
|
||||||
<div class="title pb-2">Coolify</div>
|
|
||||||
<div class="flex flex-wrap justify-center">
|
|
||||||
{#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true) as buildPack}
|
|
||||||
<div class="p-2">
|
|
||||||
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="max-w-5xl mx-auto ">
|
|
||||||
<div class="title pb-2">Other</div>
|
<div class="title pb-2">Other</div>
|
||||||
<div class="flex flex-wrap justify-center">
|
<div class="flex flex-wrap justify-center">
|
||||||
{#each buildPacks.filter((bp) => bp.isHerokuBuildPack === true) as buildPack}
|
{#each buildPacks.filter((bp) => bp.isHerokuBuildPack === true) as buildPack}
|
||||||
@@ -288,4 +271,24 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="max-w-screen-2xl mx-auto px-10">
|
||||||
|
<div class="title pb-2">Coolify Base</div>
|
||||||
|
<div class="flex flex-wrap justify-center">
|
||||||
|
{#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type ==='base') as buildPack}
|
||||||
|
<div class="p-2">
|
||||||
|
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="max-w-screen-2xl mx-auto px-10">
|
||||||
|
<div class="title pb-2">Coolify Specific</div>
|
||||||
|
<div class="flex flex-wrap justify-center">
|
||||||
|
{#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type ==='specific') as buildPack}
|
||||||
|
<div class="p-2">
|
||||||
|
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -126,7 +126,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="mx-auto max-w-4xl p-6">
|
<div class="mx-auto max-w-6xl p-6">
|
||||||
<div class="grid grid-flow-row gap-2 px-10">
|
<div class="grid grid-flow-row gap-2 px-10">
|
||||||
<div class="font-bold text-xl tracking-tight">Connect a Hosted / Remote Database</div>
|
<div class="font-bold text-xl tracking-tight">Connect a Hosted / Remote Database</div>
|
||||||
<div class="mt-2 grid grid-cols-2 items-center px-4">
|
<div class="mt-2 grid grid-cols-2 items-center px-4">
|
||||||
|
|||||||
@@ -63,19 +63,14 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex space-x-1 p-6 font-bold">
|
<div class="flex flex-col justify-center w-full">
|
||||||
<div class="mr-4 text-2xl tracking-tight">
|
|
||||||
{$t('application.configuration.configure_destination')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col justify-center">
|
|
||||||
{#if !destinations || ownDestinations.length === 0}
|
{#if !destinations || ownDestinations.length === 0}
|
||||||
<div class="flex-col">
|
<div class="flex-col">
|
||||||
<div class="pb-2 text-center font-bold">
|
<div class="pb-2 text-center font-bold">
|
||||||
{$t('application.configuration.no_configurable_destination')}
|
{$t('application.configuration.no_configurable_destination')}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
|
<a href="/destinations/new" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
|
||||||
<svg
|
<svg
|
||||||
class="w-6"
|
class="w-6"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -93,7 +88,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row mx-auto">
|
||||||
{#each ownDestinations as destination}
|
{#each ownDestinations as destination}
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<form on:submit|preventDefault={() => handleSubmit(destination.id)}>
|
<form on:submit|preventDefault={() => handleSubmit(destination.id)}>
|
||||||
@@ -106,9 +101,9 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{#if otherDestinations.length > 0 && $appSession.teamId === '0'}
|
{#if otherDestinations.length > 0 && $appSession.teamId === '0'}
|
||||||
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Destinations</div>
|
<div class="px-6 pb-5 pt-10 title">Other Destinations</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row mx-auto">
|
||||||
{#each otherDestinations as destination}
|
{#each otherDestinations as destination}
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<form on:submit|preventDefault={() => handleSubmit(destination.id)}>
|
<form on:submit|preventDefault={() => handleSubmit(destination.id)}>
|
||||||
|
|||||||
@@ -36,16 +36,8 @@
|
|||||||
import GitlabRepositories from './_GitlabRepositories.svelte';
|
import GitlabRepositories from './_GitlabRepositories.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex space-x-1 p-6 font-bold">
|
{#if application.gitSource.type === 'github'}
|
||||||
<div class="mr-4 text-2xl tracking-tight">
|
<GithubRepositories {application} />
|
||||||
{$t('application.configuration.select_a_repository_project')}
|
{:else if application.gitSource.type === 'gitlab'}
|
||||||
</div>
|
<GitlabRepositories {application} {appId} {settings} />
|
||||||
</div>
|
{/if}
|
||||||
<div class="flex flex-wrap justify-center">
|
|
||||||
{#if application.gitSource.type === 'github'}
|
|
||||||
<GithubRepositories {application} />
|
|
||||||
{:else if application.gitSource.type === 'gitlab'}
|
|
||||||
<GitlabRepositories {application} {appId} {settings} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|||||||
@@ -68,12 +68,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex space-x-1 p-6 font-bold">
|
<div class="max-w-screen-2xl mx-auto px-9">
|
||||||
<div class="mr-4 text-2xl tracking-tight">
|
|
||||||
{$t('application.configuration.select_a_git_source')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="max-w-5xl mx-auto ">
|
|
||||||
<div class="title pb-8">Git App</div>
|
<div class="title pb-8">Git App</div>
|
||||||
<div class="flex flex-wrap justify-center">
|
<div class="flex flex-wrap justify-center">
|
||||||
{#if !filteredSources || ownSources.length === 0}
|
{#if !filteredSources || ownSources.length === 0}
|
||||||
@@ -103,7 +98,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row ">
|
<div class="flex flex-col lg:flex-row lg:flex-wrap justify-center">
|
||||||
{#each ownSources as source}
|
{#each ownSources as source}
|
||||||
<div class="p-2 relative">
|
<div class="p-2 relative">
|
||||||
<div class="absolute -m-4">
|
<div class="absolute -m-4">
|
||||||
@@ -147,7 +142,7 @@
|
|||||||
<button
|
<button
|
||||||
disabled={source.gitlabApp && !source.gitlabAppId}
|
disabled={source.gitlabApp && !source.gitlabAppId}
|
||||||
type="submit"
|
type="submit"
|
||||||
class="disabled:opacity-95 bg-coolgray-200 disabled:text-white box-selection hover:bg-orange-700 group"
|
class="disabled:opacity-95 bg-coolgray-200 disabled:text-white box-selection hover:bg-orange-700 group w-full lg:w-96"
|
||||||
class:border-red-500={source.gitlabApp && !source.gitlabAppId}
|
class:border-red-500={source.gitlabApp && !source.gitlabAppId}
|
||||||
class:border-0={source.gitlabApp && !source.gitlabAppId}
|
class:border-0={source.gitlabApp && !source.gitlabAppId}
|
||||||
class:border-l-4={source.gitlabApp && !source.gitlabAppId}
|
class:border-l-4={source.gitlabApp && !source.gitlabAppId}
|
||||||
@@ -192,7 +187,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex flex-row items-center">
|
||||||
<div class="title py-4">Public Repository</div>
|
<div class="title py-4">Public Repository</div>
|
||||||
<DocLink url="https://docs.coollabs.io/coolify/applications/#public-repository" />
|
<DocLink url="https://docs.coollabs.io/coolify/applications/#public-repository" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
79
apps/ui/src/routes/applications/[id]/danger.svelte
Normal file
79
apps/ui/src/routes/applications/[id]/danger.svelte
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<script context="module" lang="ts">
|
||||||
|
import type { Load } from '@sveltejs/kit';
|
||||||
|
export const load: Load = async ({ fetch, params, stuff, url }) => {
|
||||||
|
try {
|
||||||
|
const response = await get(`/applications/${params.id}/secrets`);
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
application: stuff.application,
|
||||||
|
...response
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
status: 500,
|
||||||
|
error: new Error(`Could not load ${url}`)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export let application: any;
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { del, get } from '$lib/api';
|
||||||
|
import { t } from '$lib/translations';
|
||||||
|
import { appSession, status } from '$lib/store';
|
||||||
|
import { errorNotification } from '$lib/common';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
const { id } = $page.params;
|
||||||
|
|
||||||
|
let forceDelete = false;
|
||||||
|
async function deleteApplication(name: string, force: boolean) {
|
||||||
|
const sure = confirm($t('application.confirm_to_delete', { name }));
|
||||||
|
if (sure) {
|
||||||
|
$status.application.initialLoading = true;
|
||||||
|
try {
|
||||||
|
await del(`/applications/${id}`, { id, force });
|
||||||
|
return await goto('/')
|
||||||
|
} catch (error) {
|
||||||
|
if (error.message.startsWith(`Command failed: SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid`)) {
|
||||||
|
forceDelete = true;
|
||||||
|
}
|
||||||
|
return errorNotification(error);
|
||||||
|
} finally {
|
||||||
|
$status.application.initialLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mx-auto w-full">
|
||||||
|
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
|
||||||
|
<div class="title font-bold pb-3">Danger Zone</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if forceDelete}
|
||||||
|
<button
|
||||||
|
id="forcedelete"
|
||||||
|
on:click={() => deleteApplication(application.name, true)}
|
||||||
|
type="submit"
|
||||||
|
disabled={!$appSession.isAdmin}
|
||||||
|
class:bg-red-600={$appSession.isAdmin}
|
||||||
|
class:hover:bg-red-500={$appSession.isAdmin}
|
||||||
|
class="btn btn-sm btn-error text-sm"
|
||||||
|
>
|
||||||
|
Force Delete Application
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
id="delete"
|
||||||
|
on:click={() => deleteApplication(application.name, false)}
|
||||||
|
type="submit"
|
||||||
|
disabled={!$appSession.isAdmin}
|
||||||
|
class="btn btn-lg btn-error hover:bg-red-700 text-sm"
|
||||||
|
>
|
||||||
|
Delete Application
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
160
apps/ui/src/routes/applications/[id]/features.svelte
Normal file
160
apps/ui/src/routes/applications/[id]/features.svelte
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
<script context="module" lang="ts">
|
||||||
|
import type { Load } from '@sveltejs/kit';
|
||||||
|
export const load: Load = async ({ fetch, params, stuff, url }) => {
|
||||||
|
try {
|
||||||
|
if (stuff?.application?.id) {
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
application: stuff.application,
|
||||||
|
settings: stuff.settings
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const response = await get(`/applications/${params.id}`);
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
...response
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
status: 500,
|
||||||
|
error: new Error(`Could not load ${url}`)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export let application: any;
|
||||||
|
export let settings: any;
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { get, post } from '$lib/api';
|
||||||
|
import {
|
||||||
|
addToast,
|
||||||
|
appSession,
|
||||||
|
checkIfDeploymentEnabledApplications,
|
||||||
|
setLocation,
|
||||||
|
status,
|
||||||
|
isDeploymentEnabled
|
||||||
|
} from '$lib/store';
|
||||||
|
import { t } from '$lib/translations';
|
||||||
|
import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common';
|
||||||
|
import Setting from '$lib/components/Setting.svelte';
|
||||||
|
|
||||||
|
const { id } = $page.params;
|
||||||
|
|
||||||
|
let debug = application.settings.debug;
|
||||||
|
let previews = application.settings.previews;
|
||||||
|
let dualCerts = application.settings.dualCerts;
|
||||||
|
let autodeploy = application.settings.autodeploy;
|
||||||
|
let isBot = application.settings.isBot;
|
||||||
|
let isDBBranching = application.settings.isDBBranching;
|
||||||
|
|
||||||
|
async function changeSettings(name: any) {
|
||||||
|
if (name === 'debug') {
|
||||||
|
debug = !debug;
|
||||||
|
}
|
||||||
|
if (name === 'previews') {
|
||||||
|
previews = !previews;
|
||||||
|
}
|
||||||
|
if (name === 'dualCerts') {
|
||||||
|
dualCerts = !dualCerts;
|
||||||
|
}
|
||||||
|
if (name === 'autodeploy') {
|
||||||
|
autodeploy = !autodeploy;
|
||||||
|
}
|
||||||
|
if (name === 'isBot') {
|
||||||
|
if ($status.application.isRunning) return;
|
||||||
|
isBot = !isBot;
|
||||||
|
application.settings.isBot = isBot;
|
||||||
|
application.fqdn = null;
|
||||||
|
setLocation(application, settings);
|
||||||
|
}
|
||||||
|
if (name === 'isDBBranching') {
|
||||||
|
isDBBranching = !isDBBranching;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await post(`/applications/${id}/settings`, {
|
||||||
|
previews,
|
||||||
|
debug,
|
||||||
|
dualCerts,
|
||||||
|
isBot,
|
||||||
|
autodeploy,
|
||||||
|
isDBBranching,
|
||||||
|
branch: application.branch,
|
||||||
|
projectId: application.projectId
|
||||||
|
});
|
||||||
|
return addToast({
|
||||||
|
message: $t('application.settings_saved'),
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (name === 'debug') {
|
||||||
|
debug = !debug;
|
||||||
|
}
|
||||||
|
if (name === 'previews') {
|
||||||
|
previews = !previews;
|
||||||
|
}
|
||||||
|
if (name === 'dualCerts') {
|
||||||
|
dualCerts = !dualCerts;
|
||||||
|
}
|
||||||
|
if (name === 'autodeploy') {
|
||||||
|
autodeploy = !autodeploy;
|
||||||
|
}
|
||||||
|
if (name === 'isBot') {
|
||||||
|
isBot = !isBot;
|
||||||
|
}
|
||||||
|
if (name === 'isDBBranching') {
|
||||||
|
isDBBranching = !isDBBranching;
|
||||||
|
}
|
||||||
|
return errorNotification(error);
|
||||||
|
} finally {
|
||||||
|
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full">
|
||||||
|
<div class="mx-auto w-full">
|
||||||
|
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
|
||||||
|
<div class="title font-bold pb-3">Features</div>
|
||||||
|
</div>
|
||||||
|
<div class="px-4 lg:pb-10 pb-6">
|
||||||
|
{#if !application.settings.isPublicRepository}
|
||||||
|
<div class="grid grid-cols-2 items-center">
|
||||||
|
<Setting
|
||||||
|
id="autodeploy"
|
||||||
|
isCenter={false}
|
||||||
|
bind:setting={autodeploy}
|
||||||
|
on:click={() => changeSettings('autodeploy')}
|
||||||
|
title={$t('application.enable_automatic_deployment')}
|
||||||
|
description={$t('application.enable_auto_deploy_webhooks')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if !application.settings.isBot && !application.settings.isPublicRepository}
|
||||||
|
<div class="grid grid-cols-2 items-center">
|
||||||
|
<Setting
|
||||||
|
id="previews"
|
||||||
|
isCenter={false}
|
||||||
|
bind:setting={previews}
|
||||||
|
on:click={() => changeSettings('previews')}
|
||||||
|
title={$t('application.enable_mr_pr_previews')}
|
||||||
|
description={$t('application.enable_preview_deploy_mr_pr_requests')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="grid grid-cols-2 items-center w-full">
|
||||||
|
<Setting
|
||||||
|
id="debug"
|
||||||
|
isCenter={false}
|
||||||
|
bind:setting={debug}
|
||||||
|
on:click={() => changeSettings('debug')}
|
||||||
|
title={$t('application.debug_logs')}
|
||||||
|
description={$t('application.enable_debug_log_during_build')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user