Compare commits

...

126 Commits

Author SHA1 Message Date
Andras Bacsai
5ccea1cfcc Merge pull request #370 from coollabsio/next
v2.5.2
2022-04-25 17:49:07 +02:00
Andras Bacsai
8ccb1bd34c show autoupdate in localhost 2022-04-25 17:48:25 +02:00
Andras Bacsai
c1a48dcf1e feat: Autoupdater 2022-04-25 15:51:43 +02:00
Andras Bacsai
11d74c0c1f feat: Coolify auto-updater 2022-04-25 09:54:28 +02:00
Andras Bacsai
8290ee856f migration for umami 2022-04-25 09:11:49 +02:00
Andras Bacsai
08332c8321 fix: Contribution guide 2022-04-25 08:55:04 +02:00
Andras Bacsai
046f738b7d feat: Umami service 2022-04-25 08:54:53 +02:00
Andras Bacsai
07708155ac WIP: Umami service 2022-04-25 00:00:06 +02:00
Andras Bacsai
df5e23c7c2 fix: Contribution guide 2022-04-24 00:27:27 +02:00
Andras Bacsai
41adc02801 fix: Contribution 2022-04-24 00:25:38 +02:00
Andras Bacsai
72b650b086 fix: Simplify list services 2022-04-24 00:24:08 +02:00
Andras Bacsai
06fe3f33c0 fix: Contribution guide 2022-04-24 00:23:35 +02:00
Andras Bacsai
cbabf7fc51 chore: version++ 2022-04-23 18:46:00 +02:00
Andras Bacsai
6aeafda604 fix: Reactivate posgtres password 2022-04-23 16:12:16 +02:00
Andras Bacsai
30d656698e Merge pull request #369 from coollabsio/next
v2.5.1
2022-04-23 13:16:35 +02:00
Andras Bacsai
94d1af01df Merge pull request #365 from coollabsio/restray-restray_i18n
v2.5.1
2022-04-23 13:15:13 +02:00
Andras Bacsai
af97d399b6 fix: Code cleanups 2022-04-22 13:57:28 +02:00
Andras Bacsai
2f90fd1fe6 fix: No logs found 2022-04-22 11:44:04 +02:00
Andras Bacsai
c05a140b0b fix: GitHub token cleanup on team switch 2022-04-22 11:43:55 +02:00
Andras Bacsai
cbfb9a3844 chore: version++ 2022-04-22 11:20:15 +02:00
Andras Bacsai
5a227f70c6 fix: Do not activate i18n for now 2022-04-22 11:19:56 +02:00
Andras Bacsai
44a102443d fix: Application logs is not reversed and queried better 2022-04-21 23:07:54 +02:00
Andras Bacsai
cf7fdf198d fix: locales 2022-04-21 14:57:52 +02:00
Andras Bacsai
68f2f4f978 fix: Vscode permission fix 2022-04-21 11:25:51 +02:00
Andras Bacsai
029b623f08 fix: i18n 2022-04-21 10:05:27 +02:00
Andras Bacsai
fe3702847a Merge branch 'restray_i18n' of https://github.com/restray/coolify into restray-restray_i18n 2022-04-21 09:51:29 +02:00
Andras Bacsai
e9b852a30e Merge pull request #361 from coollabsio/next
v2.5.0
2022-04-20 23:14:54 +02:00
Andras Bacsai
1d4e5df5a2 fix contribution guide 2022-04-20 23:14:19 +02:00
Andras Bacsai
5e14b72fe4 Merge pull request #362 from coollabsio/contribution
Extended contribution guide
2022-04-20 23:08:50 +02:00
Andras Bacsai
8ebff72cde fix: Correct branch shown in build logs 2022-04-20 23:02:19 +02:00
Andras Bacsai
e16643c48c feat: Query container state periodically 2022-04-20 22:49:24 +02:00
Andras Bacsai
65c8f55ee6 fix: Text on deno buildpack 2022-04-20 22:27:10 +02:00
Andras Bacsai
fbc81ab3eb feat/fix: Show exited containers on UI & better UX 2022-04-20 22:24:41 +02:00
Andras Bacsai
a4d56fd79a feat: Deno DB migration 2022-04-20 22:24:00 +02:00
Andras Bacsai
ce45cb8aca package updates 2022-04-20 22:23:35 +02:00
Andras Bacsai
7f8428cd17 fix: Deno configurations 2022-04-20 22:23:25 +02:00
Andras Bacsai
14d79031c1 Merge pull request #360 from lichtscheu/buildpack-deno
Buildpack Deno
2022-04-20 19:06:27 +02:00
Andras Bacsai
b8aa7b6d08 Internal changes 2022-04-20 15:15:18 +02:00
Andras Bacsai
397ca7f20e Merge pull request #357 from coollabsio/next
v2.4.11
2022-04-20 14:19:49 +02:00
Andras Bacsai
e10b76a46b feat: Fluentbit investigation 2022-04-20 13:33:04 +02:00
Andras Bacsai
b46566280d fix: Application logs 2022-04-20 09:23:06 +02:00
Andras Bacsai
3ab6a231eb feat: Testing fluentd logging driver 2022-04-20 00:20:37 +02:00
lichtscheu
2bc2ae9b6e Merge remote-tracking branch 'upstream/next' into buildpack-deno
# Conflicts:
#	src/routes/applications/[id]/index.svelte
2022-04-19 23:17:03 +02:00
Andras Bacsai
2b28f8bd8f feat: Multiply dockerfile locations for docker buildpack 2022-04-19 22:34:28 +02:00
lichtscheu
dcdac29135 Merge remote-tracking branch 'upstream/main' into buildpack-deno 2022-04-19 22:09:21 +02:00
lichtscheu
591ee29e0d feat: initial deno support 2022-04-19 22:08:42 +02:00
Andras Bacsai
625e71ab08 fix: white-labeled custom logo 2022-04-19 18:23:04 +02:00
Andras Bacsai
b0af54587b feat: Add persistent storage for services 2022-04-18 23:49:08 +02:00
Andras Bacsai
be3080df08 fix: Pull new images for services all the time it's started. 2022-04-18 22:51:55 +02:00
Andras Bacsai
04685c9f9d fix: Scroll to top for logs 2022-04-18 22:46:39 +02:00
Andras Bacsai
1a83f2635f fix: Switch to stream on applications logs 2022-04-18 00:44:08 +02:00
Andras Bacsai
630aa45c87 fix: Application logs paginated 2022-04-18 00:42:08 +02:00
Andras Bacsai
0c3a381d1f fix: Buildlog line number is not string 2022-04-17 23:32:27 +02:00
Andras Bacsai
ffac7c5c87 chore:version++ 2022-04-17 21:08:19 +02:00
Andras Bacsai
410800e81c fix: use arm based certbot on arm 2022-04-17 21:07:59 +02:00
Andras Bacsai
9481beb61f Merge pull request #355 from coollabsio/next
v2.4.10
2022-04-17 20:37:56 +02:00
Andras Bacsai
141f2481a7 fix: Change user's id in sftp wp instance 2022-04-17 20:22:42 +02:00
Andras Bacsai
ea18f25adc ui: show extraconfig if wp is running 2022-04-17 20:22:21 +02:00
Andras Bacsai
9018184747 fix: Stop sFTP connection on wp stop 2022-04-17 20:22:07 +02:00
Andras Bacsai
4fc2dd55f5 chore: version++ 2022-04-17 19:17:20 +02:00
Andras Bacsai
5ef9a282eb fix: Wordpress extra config 2022-04-17 19:17:12 +02:00
Andras Bacsai
93a6518974 docs: update 2022-04-16 23:04:29 +02:00
Andras Bacsai
07aa285b27 updates on docs 2022-04-16 22:31:30 +02:00
Andras Bacsai
bf01e9e29f Grammar things 2022-04-16 22:28:17 +02:00
Andras Bacsai
d70672ba4b switch example type 2022-04-16 22:21:42 +02:00
Andras Bacsai
5eeb519ed6 docs: update 2022-04-16 22:19:41 +02:00
Andras Bacsai
5f047e4adf docs: How to add new services 2022-04-16 22:16:47 +02:00
Andras Bacsai
56b9a376bd fix: use redis-alpine 2022-04-14 23:48:52 +02:00
Andras Bacsai
0a1d31a188 Merge pull request #349 from coollabsio/v2.4.9
fix: Switch from bitnami/redis to normal redis
2022-04-14 23:42:13 +02:00
Andras Bacsai
64c9fb9a1b fix: Switch from bitnami/redis to normal redis 2022-04-14 23:40:23 +02:00
Andras Bacsai
47aad15cd5 Merge pull request #347 from coollabsio/v2.4.9
v2.4.9
2022-04-14 23:29:15 +02:00
Andras Bacsai
260a47a366 fix: Id of service container 2022-04-14 23:11:24 +02:00
Andras Bacsai
fd4bbe17f0 fix: Restart local docker coolify proxy in case of something happens to it 2022-04-14 21:43:22 +02:00
Andras Bacsai
25ff637703 fix: Remove proxy container in case of dependent container is down 2022-04-14 21:43:05 +02:00
Andras Bacsai
f571453696 fix: Better performance for cleanup images 2022-04-14 18:45:42 +02:00
Andras Bacsai
5cd7533972 fix: Loading of new destinations 2022-04-14 18:34:43 +02:00
Andras Bacsai
3a252509d0 fix: Add HTTP proxy checks 2022-04-14 15:04:18 +02:00
Andras Bacsai
2bd3802a6f fix: Improved tcp proxy monitoring for databases/ftp 2022-04-14 00:04:46 +02:00
Andras Bacsai
ce2757f514 fix: Teams view 2022-04-13 21:06:22 +02:00
Andras Bacsai
8419cdf604 fix: Postgres root pw is pw field 2022-04-13 19:59:30 +02:00
Andras Bacsai
907c2414ae chore:version++ 2022-04-13 19:52:56 +02:00
Andras Bacsai
f82207564f Merge pull request #344 from coollabsio/v2.4.8
v2.4.8
2022-04-13 19:19:04 +02:00
Andras Bacsai
991a09838c chore: version++ 2022-04-13 16:08:40 +02:00
Andras Bacsai
25df4bfd85 fix: Remove system wide pw reset 2022-04-13 16:05:26 +02:00
Andras Bacsai
d2f89d001b fix: GitLab typo 2022-04-13 16:05:08 +02:00
Andras Bacsai
1971f227fd fix: Register should happen if coolify proxy cannot be started 2022-04-13 14:23:42 +02:00
Andras Bacsai
c1adffe260 Merge pull request #343 from coollabsio/v2.4.7
v2.4.7
2022-04-13 13:12:35 +02:00
Andras Bacsai
e725887a55 chore:version++ 2022-04-13 13:12:23 +02:00
Andras Bacsai
5bf79b75b0 fix: Destinations to HAProxy 2022-04-13 13:10:04 +02:00
Andras Bacsai
6926975e40 Merge pull request #341 from coollabsio/v2.4.6
v2.4.6
2022-04-13 08:40:10 +02:00
Andras Bacsai
978a01c968 fix: Reverting postgres password for now 2022-04-13 08:35:20 +02:00
Andras Bacsai
f421f5ee84 fix: No permission on first registration 2022-04-12 23:57:08 +02:00
Andras Bacsai
383831c7b8 fix: Restart policy for resources 2022-04-12 23:12:09 +02:00
Andras Bacsai
41329facf7 fix: Try catch me 2022-04-12 22:49:48 +02:00
Andras Bacsai
7d3c644148 fix: DNS check before creating SSL cert 2022-04-12 22:18:54 +02:00
Andras Bacsai
7fab9b5930 fix: ProjectID for Github 2022-04-12 22:18:43 +02:00
Andras Bacsai
58763ef84c fix: Load all branches, not just the first 30 2022-04-12 21:48:50 +02:00
Andras Bacsai
0e6abf172b fix: Meilisearch service 2022-04-12 21:09:38 +02:00
Andras Bacsai
9e681ece41 chore: version++ 2022-04-12 20:58:02 +02:00
Andras Bacsai
28f87a306d fix: Cleanup images older than a day 2022-04-12 20:57:49 +02:00
Restray
c39cb42601 feat (i18n) : go back i18n loading json files 2022-04-04 17:56:32 +02:00
Restray
0ead17ab70 Patch translation module not loaded 2022-04-04 17:06:03 +02:00
Restray
4a6062522e Merge branch 'main' into restray_i18n 2022-04-04 16:54:51 +02:00
Restray
bd15d85732 Add last translations for 2.3.0 2022-04-04 12:37:24 +02:00
Restray
b4bbd22781 Merge branch 'restray_i18n' of github.com:restray/coolify into restray_i18n 2022-04-04 12:31:48 +02:00
Restray
d4c972584a Add english translation for register page 2022-04-04 12:30:22 +02:00
Restray
edef4bd4a0 Merge branch 'main' into restray_i18n 2022-04-04 12:29:20 +02:00
Restray
448611039c Patch langs problems 2022-04-04 11:50:54 +02:00
Restray
e4f701b148 Add auto detect of locales files and contrib guide 2022-04-03 21:47:58 +02:00
Restray
8cd561b8cc Update french translations 2022-04-03 19:58:10 +02:00
Restray
a284928352 Patch flags and add french translation 2022-04-03 14:34:48 +02:00
Restray
fe787538e3 Finish routes translations 2022-04-03 14:14:59 +02:00
Restray
360fb5ea37 Add services i18n 2022-04-03 00:18:48 +02:00
Restray
13891110ce Add reset i18n 2022-04-02 23:57:37 +02:00
Restray
c1c25d59c8 Add "new" i18n 2022-04-02 23:53:10 +02:00
Restray
a53bda1436 Add destinations i18n 2022-04-02 23:34:30 +02:00
Restray
7a0d151467 Add database translation 2022-04-02 23:17:59 +02:00
Restray
a788b7bc13 Add translation for applications components 2022-04-02 23:00:03 +02:00
Restray
8f58b14629 Add application i18n 2022-04-02 22:04:50 +02:00
Restray
269250ef3d Begin translation and finish i18n system 2022-04-02 21:08:55 +02:00
Restray
a3241516cb Change the way to load i18n (go throw cookie) 2022-04-02 20:25:24 +02:00
Restray
943300509b Revert "Add Locale URL"
This reverts commit d910b21185.
2022-04-02 19:29:22 +02:00
Restray
92d1f5aa55 Revert "Patch bugs on locale redirections"
This reverts commit 614eb923d8.
2022-04-02 19:28:20 +02:00
Restray
614eb923d8 Patch bugs on locale redirections 2022-04-02 17:33:50 +02:00
Restray
d910b21185 Add Locale URL 2022-04-02 16:15:00 +02:00
Restray
741db1778b feat: install svelte-18n and init setup 2022-04-01 22:50:55 +02:00
165 changed files with 4085 additions and 1432 deletions

View File

@@ -2,5 +2,7 @@ COOLIFY_APP_ID=
COOLIFY_SECRET_KEY=12341234123412341234123412341234
COOLIFY_DATABASE_URL=file:../db/dev.db
COOLIFY_SENTRY_DSN=
COOLIFY_IS_ON="docker"
COOLIFY_WHITE_LABELED="false"
COOLIFY_IS_ON=docker
COOLIFY_WHITE_LABELED=false
COOLIFY_WHITE_LABELED_ICON=
COOLIFY_AUTO_UPDATE=false

11
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"i18n-ally.localesPaths": ["src/lib/locales"],
"i18n-ally.keystyle": "nested",
"i18n-ally.extract.ignoredByFiles": {
"src\\routes\\__layout.svelte": ["Coolify", "coolLabs logo"]
},
"i18n-ally.sourceLanguage": "en",
"i18n-ally.enabledFrameworks": ["svelte"],
"i18n-ally.enabledParsers": ["js", "ts", "json"],
"i18n-ally.extract.autoDetect": true
}

View File

@@ -1,14 +1,23 @@
# Welcome
# 👋 Welcome
First of all, thank you for considering to contribute to my project! It means a lot 💜.
First of all, thank you for considering contributing to my project! It means a lot 💜.
# Technical skills required
## 🙋 Want to help?
- Node.js / Javascript
- Svelte / SvelteKit
- Prisma.io
If you begin in GitHub contribution, you can find the [first contribution](https://github.com/firstcontributions/first-contributions) and follow this guide.
# Recommended Pull Request Guideline
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
🔴 At the moment, Coolify **doesn't support Windows**. You must use Linux or MacOS.
#### Recommended Pull Request Guideline
- Fork the project
- Clone your fork repo to local
@@ -16,15 +25,17 @@ First of all, thank you for considering to contribute to my project! It means a
- Push to your fork repo
- Create a pull request: https://github.com/coollabsio/compare
- Write a proper description
- Open the pull request to review
- Open the pull request to review against `next` branch
---
# How to start after you set up your local fork?
This repository best with [pnpm](https://pnpm.io) due to the lock file. I recommend you should try and use `pnpm` as well, because it is cool and efficient!
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!
You need to have [Docker Engine](https://docs.docker.com/engine/install/) installed locally.
## Setup development environment
#### Setup a local development environment
- Copy `.env.template` to `.env` and set the `COOLIFY_APP_ID` environment variable to something cool.
- Install dependencies with `pnpm install`.
@@ -33,12 +44,234 @@ You need to have [Docker Engine](https://docs.docker.com/engine/install/) instal
- Seed the database with base entities with `pnpm db:seed`
- You can start coding after starting `pnpm dev`.
## Database migrations
#### How to start after you set up your local fork?
This repository works better with [pnpm](https://pnpm.io) due to the lock file. I recommend you to give it a try and use `pnpm` as well because it is cool and efficient!
You need to have [Docker Engine](https://docs.docker.com/engine/install/) installed locally.
## 🧑‍💻 Developer contribution
### Technical skills required
- **Languages**: Node.js / Javascript / Typescript
- **Framework JS/TS**: Svelte / SvelteKit
- **Database ORM**: Prisma.io
- **Docker Engine**
### 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. :)
## Tricky parts
### Tricky parts
- BullMQ, the queue system Coolify is using, cannot be hot reloaded. So if you change anything in the files related to it, you need to restart the development process. I'm actively looking of a different queue/scheduler library. I'm open for discussion!
- BullMQ, the queue system Coolify uses, cannot be hot reloaded. So if you change anything in the files related to it, you need to restart the development process. I'm actively looking for a different queue/scheduler library. I'm open to discussion!
---
# 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 relationshup with `Service` model.
- In the `Service` model, the name of the new field should be with low-capital.
- If the service needs a database, define a `publicPort` field to be able to make it's database public, example field name in case of PostgreSQL: `postgresqlPublicPort`. It should be a optional field.
If you are finished with the Prisma schema, you should update the database schema with `pnpm db:push` command.
> You must restart the running development environment to be able to use the new model
> If you use VSCode, you probably need to restart the `Typescript Language Server` to get the new types loaded in the running VSCode.
### Add supported versions
Supported versions are hardcoded into Coolify (for now).
You need to update `supportedServiceTypesAndVersions` function at [src/lib/components/common.ts](src/lib/components/common.ts). Example JSON:
```js
{
// Name used to identify the service 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
}
}
```
### Update global functions
1. Add the new service to the `include` variable in [src/lib/database/services.ts](src/lib/database/services.ts), so it will be included in all places in the database queries where it is required.
```js
const include: Prisma.ServiceInclude = {
destinationDocker: true,
persistentStorage: true,
serviceSecret: true,
minio: true,
plausibleAnalytics: true,
vscodeserver: true,
wordpress: true,
ghost: true,
meiliSearch: true,
umami: true // This line!
};
```
2. Update the database update query with the new service type to `configureServiceType` function in [src/lib/database/services.ts](src/lib/database/services.ts). This function defines the automatically generated variables (passwords, users, etc.) and it's encryption process (if applicable).
```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 decryption process for configurations and passwords to `getService` function in [src/lib/database/services.ts](src/lib/database/services.ts)
```js
if (body.umami?.postgresqlPassword)
body.umami.postgresqlPassword = decrypt(body.umami.postgresqlPassword);
if (body.umami?.hashSalt) body.umami.hashSalt = decrypt(body.umami.hashSalt);
```
4. Add service deletion query to `removeService` function in [src/lib/database/services.ts](src/lib/database/services.ts)
### Create API endpoints.
You need to add a new folder under [src/routes/services/[id]](src/routes/services/[id]) with the low-capital name of the service. You need 3 default files in that folder.
#### `index.json.ts`:
It has a POST endpoint that updates the service details in Coolify's database, such as name, url, other configurations, like passwords. It should look something like this:
```js
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
let { name, fqdn } = await event.request.json();
if (fqdn) fqdn = fqdn.toLowerCase();
try {
await db.updateService({ id, fqdn, name });
return { status: 201 };
} catch (error) {
return ErrorHandler(error);
}
};
```
If it's necessary, you can create your own database update function, specifically for the new service.
#### `start.json.ts`
It has a POST endpoint that sets all the required secrets, persistent volumes, `docker-compose.yaml` file and sends a request to the specified docker engine.
You could also define an `HTTP` or `TCP` proxy for every other port that should be proxied to your server. (See `startHttpProxy` and `startTcpProxy` functions in [src/lib/haproxy/index.ts](src/lib/haproxy/index.ts))
#### `stop.json.ts`
It has a POST endpoint that stops the service and all dependent (TCP/HTTP proxies) containers. If publicPort is specified it also needs to cleanup it from the database.
## Frontend
1. You need to add a custom logo at [src/lib/components/svg/services/](src/lib/components/svg/services/) as a svelte component.
SVG is recommended, but you can use PNG as well. It should have the `isAbsolute` variable with the suitable CSS classes, primarily for sizing and positioning.
2. You need to include it the logo at
- [src/routes/services/index.svelte](src/routes/services/index.svelte) with `isAbsolute` in two places,
- [src/lib/components/ServiceLinks.svelte](src/lib/components/ServiceLinks.svelte) with `isAbsolute` and a link to the docs/main site of the service
- [src/routes/services/[id]/configuration/type.svelte](src/routes/services/[id]/configuration/type.svelte) with `isAbsolute`.
3. By default the URL and the name frontend forms are included in [src/routes/services/[id]/\_Services/\_Services.svelte](src/routes/services/[id]/_Services/_Services.svelte).
If you need to show more details on the frontend, such as users/passwords, you need to add Svelte component to [src/routes/services/[id]/\_Services](src/routes/services/[id]/_Services) with an underscore. For example, see other files in that folder.
You also need to add the new inputs to the `index.json.ts` file of the specific service, like for MinIO here: [src/routes/services/[id]/minio/index.json.ts](src/routes/services/[id]/minio/index.json.ts)
## 🌐 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!

View File

@@ -22,7 +22,7 @@ If you would like no questions during installation
### Git Sources
You can use the following Git Sources to be auto-deployed to your Coolifyt instance! (Self hosted versions also supported.)
You can use the following Git Sources to be auto-deployed to your Coolifyt instance! (Self-hosted versions are also supported.)
- Github
- GitLab
@@ -38,7 +38,7 @@ You can deploy your applications to the following destinations:
### Applications
These are the predefined build packs, but with the Docker build pack, you can host basically anything that is hostable with a single Dockerfile.
These are the predefined build packs, but with the Docker build pack, you can host anything that is hostable with a single Dockerfile.
- Static sites
- NodeJS

View File

@@ -1 +0,0 @@
nohup docker build -t coollabsio/prisma-engine:<arm64/amd64> --push . &

View File

@@ -0,0 +1,6 @@
FROM fluent/fluent-bit:1.9.0
COPY fluentbit-dev.conf /tmp/fluentbit.conf
ENTRYPOINT ["/fluent-bit/bin/fluent-bit", "-c", "/tmp/fluentbit.conf"]
# USER root
# RUN ["gem", "install", "fluent-plugin-mongo"]
# USER fluent

View File

@@ -0,0 +1,24 @@
[INPUT]
Name forward
Listen 0.0.0.0
Port 24224
Buffer_Chunk_Size 32KB
Buffer_Max_Size 64KB
[OUTPUT]
Name influxdb
Match *
Host coolify-influxdb
Port 8086
Bucket containerlogs
Org organization
HTTP_Token supertoken
Sequence_Tag _seq
Tag_Keys container_name
[OUTPUT]
Name http
Match *
Host host.docker.internal
Port 3000
URI /logs.json
Format json

View File

@@ -0,0 +1,28 @@
<source>
@type forward
port 24224
bind 0.0.0.0
</source>
<match **>
@type http
endpoint http://host.docker.internal:3000/logs.json
<buffer>
flush_at_shutdown true
flush_mode immediate
flush_thread_count 8
flush_thread_interval 1
flush_thread_burst_interval 1
retry_forever true
retry_type exponential_backoff
</buffer>
</match>
<filter docker.**>
@type parser
key_name log
reserve_data true
<parse>
@type json
</parse>
</filter>

View File

@@ -0,0 +1 @@
docker build --platform linux/amd64,linux/arm64 -t coollabsio/prisma-engine -f prisma-engine.Dockerfile --push .

View File

@@ -2,7 +2,7 @@ FROM rust:1.58.1-alpine3.14 as prisma
WORKDIR /prisma
ENV RUSTFLAGS="-C target-feature=-crt-static"
RUN apk --no-cache add openssl direnv git musl-dev openssl-dev build-base perl protoc
RUN git clone --depth=1 --branch=3.11.x https://github.com/prisma/prisma-engines.git /prisma
RUN git clone --depth=1 --branch=3.12.x https://github.com/prisma/prisma-engines.git /prisma
RUN cargo build --release
FROM alpine

View File

@@ -2,10 +2,8 @@ version: '3.8'
services:
redis:
image: 'bitnami/redis:6.2'
image: redis:6.2-alpine
container_name: coolify-redis
environment:
- ALLOW_EMPTY_PASSWORD=yes
networks:
- coolify-infra
ports:
@@ -13,7 +11,24 @@ services:
published: 6379
protocol: tcp
mode: host
# fluentbit:
# container_name: coolify-fluentbit
# build:
# context: ./data/fluentd
# dockerfile: Dockerfile-dev
# ports:
# - target: 24224
# published: 24224
# protocol: tcp
# mode: host
# - target: 24224
# published: 24224
# protocol: udp
# mode: host
# networks:
# - coolify-infra
# extra_hosts:
# - 'host.docker.internal:host-gateway'
networks:
coolify-infra:
attachable: true

View File

@@ -21,11 +21,9 @@ services:
- coolify-infra
depends_on: ['redis']
redis:
image: bitnami/redis:6.2
image: redis:6.2-alpine
restart: always
container_name: coolify-redis
environment:
- ALLOW_EMPTY_PASSWORD=yes
networks:
- coolify-infra

View File

@@ -1,10 +1,10 @@
{
"name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "2.4.5",
"version": "2.5.2",
"license": "AGPL-3.0",
"scripts": {
"dev": "docker-compose -f docker-compose-dev.yaml up -d && cross-env NODE_ENV=development & svelte-kit dev",
"dev": "docker-compose -f docker-compose-dev.yaml up -d && cross-env NODE_ENV=development & svelte-kit dev --host 0.0.0.0",
"dev:stop": "docker-compose -f docker-compose-dev.yaml down",
"dev:logs": "docker-compose -f docker-compose-dev.yaml logs -f --tail 10",
"studio": "npx prisma studio",
@@ -19,8 +19,10 @@
"db:migrate": "COOLIFY_DATABASE_URL=file:../db/migration.db prisma migrate dev --skip-seed --name",
"release:production:all": "cross-var docker build --platform linux/amd64,linux/arm64 -t coollabsio/coolify:$npm_package_version -t coollabsio/coolify:latest --push .",
"release:production:amd": "cross-var docker build --platform linux/amd64 -t coollabsio/coolify:$npm_package_version -t coollabsio/coolify:latest --push .",
"release:production:arm": "cross-var docker build --platform linux/arm64 -t coollabsio/coolify:$npm_package_version -t coollabsio/coolify:latest --push .",
"release:staging:all": "cross-var docker build --platform linux/amd64,linux/arm64 -t coollabsio/coolify:$npm_package_version --push .",
"release:staging:amd": "cross-var docker build --platform linux/amd64 -t coollabsio/coolify:$npm_package_version --push .",
"release:staging:arm": "cross-var docker build --platform linux/arm64 -t coollabsio/coolify:$npm_package_version --push .",
"release:haproxy": "docker build --platform linux/amd64,linux/arm64 -t coollabsio/coolify-haproxy-alpine:latest -t coollabsio/coolify-haproxy-alpine:1.1.0 -f data/haproxy.Dockerfile --push .",
"release:haproxy:tcp": "docker build --platform linux/amd64,linux/arm64 -t coollabsio/coolify-haproxy-tcp-alpine:latest -t coollabsio/coolify-haproxy-tcp-alpine:1.1.0 -f data/haproxy-tcp.Dockerfile --push .",
"release:haproxy:http": "docker build --platform linux/amd64,linux/arm64 -t coollabsio/coolify-haproxy-http-alpine:latest -t coollabsio/coolify-haproxy-http-alpine:1.1.0 -f data/haproxy-http.Dockerfile --push .",
@@ -28,10 +30,10 @@
},
"devDependencies": {
"@sveltejs/adapter-node": "1.0.0-next.73",
"@sveltejs/kit": "1.0.0-next.310",
"@sveltejs/kit": "1.0.0-next.316",
"@types/js-cookie": "3.0.1",
"@types/js-yaml": "4.0.5",
"@types/node": "17.0.23",
"@types/node": "17.0.25",
"@types/node-forge": "1.0.1",
"@typescript-eslint/eslint-plugin": "4.31.1",
"@typescript-eslint/parser": "4.31.1",
@@ -43,17 +45,18 @@
"eslint-config-prettier": "8.5.0",
"eslint-plugin-svelte3": "3.4.1",
"husky": "7.0.4",
"lint-staged": "12.3.7",
"lint-staged": "12.4.0",
"postcss": "8.4.12",
"prettier": "2.6.2",
"prettier-plugin-svelte": "2.7.0",
"prettier-plugin-tailwindcss": "0.1.8",
"prettier-plugin-tailwindcss": "0.1.10",
"prisma": "3.11.1",
"svelte": "3.47.0",
"svelte-check": "2.6.0",
"svelte-preprocess": "4.10.5",
"svelte-check": "2.7.0",
"svelte-preprocess": "4.10.6",
"svelte-select": "4.4.7",
"tailwindcss": "3.0.23",
"tailwindcss": "3.0.24",
"sveltekit-i18n": "2.1.2",
"ts-node": "10.7.0",
"tslib": "2.3.1",
"typescript": "4.6.3"
@@ -63,12 +66,12 @@
"@iarna/toml": "2.2.5",
"@prisma/client": "3.11.1",
"@sentry/node": "6.19.6",
"bcryptjs": "^2.4.3",
"bullmq": "1.80.0",
"bcryptjs": "2.4.3",
"bullmq": "1.80.4",
"compare-versions": "4.1.3",
"cookie": "0.4.2",
"cookie": "0.5.0",
"cuid": "2.1.8",
"dayjs": "1.11.0",
"dayjs": "1.11.1",
"dockerode": "3.3.1",
"dotenv-extended": "2.9.0",
"generate-password": "1.7.0",
@@ -80,7 +83,7 @@
"mustache": "4.2.0",
"node-forge": "1.3.1",
"p-limit": "4.0.0",
"svelte-kit-cookie-session": "2.1.2",
"svelte-kit-cookie-session": "2.1.3",
"tailwindcss-scrollbar": "0.1.0",
"unique-names-generator": "4.7.1"
},

289
pnpm-lock.yaml generated
View File

@@ -5,23 +5,23 @@ specifiers:
'@prisma/client': 3.11.1
'@sentry/node': 6.19.6
'@sveltejs/adapter-node': 1.0.0-next.73
'@sveltejs/kit': 1.0.0-next.303
'@sveltejs/kit': 1.0.0-next.316
'@types/js-cookie': 3.0.1
'@types/js-yaml': 4.0.5
'@types/node': 17.0.23
'@types/node': 17.0.25
'@types/node-forge': 1.0.1
'@typescript-eslint/eslint-plugin': 4.31.1
'@typescript-eslint/parser': 4.31.1
'@zerodevx/svelte-toast': 0.7.1
autoprefixer: 10.4.4
bcryptjs: ^2.4.3
bullmq: 1.78.1
bcryptjs: 2.4.3
bullmq: 1.80.4
compare-versions: 4.1.3
cookie: 0.4.2
cookie: 0.5.0
cross-env: 7.0.3
cross-var: 1.1.0
cuid: 2.1.8
dayjs: 1.11.0
dayjs: 1.11.1
dockerode: 3.3.1
dotenv-extended: 2.9.0
eslint: 7.32.0
@@ -34,21 +34,22 @@ specifiers:
js-cookie: 3.0.1
js-yaml: 4.1.0
jsonwebtoken: 8.5.1
lint-staged: 12.3.7
lint-staged: 12.4.0
mustache: 4.2.0
node-forge: 1.3.1
p-limit: 4.0.0
postcss: 8.4.12
prettier: 2.6.2
prettier-plugin-svelte: 2.7.0
prettier-plugin-tailwindcss: 0.1.8
prettier-plugin-tailwindcss: 0.1.10
prisma: 3.11.1
svelte: 3.47.0
svelte-check: 2.6.0
svelte-kit-cookie-session: 2.1.2
svelte-preprocess: 4.10.5
svelte-check: 2.7.0
svelte-kit-cookie-session: 2.1.3
svelte-preprocess: 4.10.6
svelte-select: 4.4.7
tailwindcss: 3.0.23
tailwindcss: 3.0.24
sveltekit-i18n: 2.1.2
tailwindcss-scrollbar: 0.1.0
ts-node: 10.7.0
tslib: 2.3.1
@@ -60,11 +61,11 @@ dependencies:
'@prisma/client': 3.11.1_prisma@3.11.1
'@sentry/node': 6.19.6
bcryptjs: 2.4.3
bullmq: 1.78.1
bullmq: 1.80.4
compare-versions: 4.1.3
cookie: 0.4.2
cookie: 0.5.0
cuid: 2.1.8
dayjs: 1.11.0
dayjs: 1.11.1
dockerode: 3.3.1
dotenv-extended: 2.9.0
generate-password: 1.7.0
@@ -76,16 +77,16 @@ dependencies:
mustache: 4.2.0
node-forge: 1.3.1
p-limit: 4.0.0
svelte-kit-cookie-session: 2.1.2
tailwindcss-scrollbar: 0.1.0_tailwindcss@3.0.23
svelte-kit-cookie-session: 2.1.3
tailwindcss-scrollbar: 0.1.0_tailwindcss@3.0.24
unique-names-generator: 4.7.1
devDependencies:
'@sveltejs/adapter-node': 1.0.0-next.73
'@sveltejs/kit': 1.0.0-next.303_svelte@3.47.0
'@sveltejs/kit': 1.0.0-next.316_svelte@3.47.0
'@types/js-cookie': 3.0.1
'@types/js-yaml': 4.0.5
'@types/node': 17.0.23
'@types/node': 17.0.25
'@types/node-forge': 1.0.1
'@typescript-eslint/eslint-plugin': 4.31.1_8ede7edd7694646e12d33c52460f622c
'@typescript-eslint/parser': 4.31.1_eslint@7.32.0+typescript@4.6.3
@@ -97,18 +98,19 @@ devDependencies:
eslint-config-prettier: 8.5.0_eslint@7.32.0
eslint-plugin-svelte3: 3.4.1_eslint@7.32.0+svelte@3.47.0
husky: 7.0.4
lint-staged: 12.3.7
lint-staged: 12.4.0
postcss: 8.4.12
prettier: 2.6.2
prettier-plugin-svelte: 2.7.0_prettier@2.6.2+svelte@3.47.0
prettier-plugin-tailwindcss: 0.1.8_prettier@2.6.2
prettier-plugin-tailwindcss: 0.1.10_prettier@2.6.2
prisma: 3.11.1
svelte: 3.47.0
svelte-check: 2.6.0_postcss@8.4.12+svelte@3.47.0
svelte-preprocess: 4.10.5_41810887ae6c6d59323116f47e33fa38
svelte-check: 2.7.0_postcss@8.4.12+svelte@3.47.0
svelte-preprocess: 4.10.6_41810887ae6c6d59323116f47e33fa38
svelte-select: 4.4.7
tailwindcss: 3.0.23_b89136460714832cdda11d1e9d57d1ff
ts-node: 10.7.0_ee885bc7281b682b6adbed6ae09ee090
tailwindcss: 3.0.24_ts-node@10.7.0
ts-node: 10.7.0_de7c86b0cde507c63a0402da5b982bd3
sveltekit-i18n: 2.1.2_svelte@3.46.4
tslib: 2.3.1
typescript: 4.6.3
@@ -374,10 +376,10 @@ packages:
tiny-glob: 0.2.9
dev: true
/@sveltejs/kit/1.0.0-next.303_svelte@3.47.0:
/@sveltejs/kit/1.0.0-next.316_svelte@3.47.0:
resolution:
{
integrity: sha512-WdxDc8OiF1WEd/bEza7CBdzA+3qIcCi1GKBj/gieKX9I3N8iDJt/Cg2POrLo9wQoJ47nZcAd1eOhfr7XEX1aIQ==
integrity: sha512-oLjWOWzjriJD2t210r7ALuH/8ZADrJGsOODzRCRSJvRBCt0Q7VKVLqwKbM/RlZzD1k8Af2uRodQT11kP98hAIA==
}
engines: { node: '>=14.13' }
hasBin: true
@@ -422,6 +424,26 @@ packages:
- supports-color
dev: true
/@sveltekit-i18n/base/1.1.1_svelte@3.46.4:
resolution:
{
integrity: sha512-J/sMU0OwS3dCLOuilHMBqu8vZHuuXiNV9vFJx8Nb4/b5BlR/KCZ4bCXI8wZR02GHeCOYKZxWus07CM1scxa/jw==
}
peerDependencies:
svelte: ^3.x
dependencies:
svelte: 3.46.4
optionalDependencies:
'@sveltekit-i18n/parser-default': 1.0.3
dev: true
/@sveltekit-i18n/parser-default/1.0.3:
resolution:
{
integrity: sha512-HheveklTjp3hxpYQhoHfyA6B4bQaUeSV5MQf2usIv/58UF2jY/YqhCAWj9bDBjufbuZc5pSz4BXvdX3WVT+viA==
}
dev: true
/@szmarczak/http-timer/5.0.1:
resolution:
{
@@ -468,7 +490,7 @@ packages:
dependencies:
'@types/http-cache-semantics': 4.0.1
'@types/keyv': 3.1.3
'@types/node': 17.0.23
'@types/node': 17.0.25
'@types/responselike': 1.0.0
dev: false
@@ -506,7 +528,7 @@ packages:
integrity: sha512-FXCJgyyN3ivVgRoml4h94G/p3kY+u/B86La+QptcqJaWtBWtmc6TtkNfS40n9bIvyLteHh7zXOtgbobORKPbDg==
}
dependencies:
'@types/node': 17.0.23
'@types/node': 17.0.25
dev: false
/@types/node-forge/1.0.1:
@@ -515,22 +537,15 @@ packages:
integrity: sha512-96ELNKv9tQJ19afdBUiM5iDw7OYEc53iUc51gAPR2aGaqRsO1DBROjqgZRjZa1tkPj7TnEOR0EnyAX6iryGkzA==
}
dependencies:
'@types/node': 17.0.23
'@types/node': 17.0.25
dev: true
/@types/node/17.0.23:
/@types/node/17.0.25:
resolution:
{
integrity: sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==
integrity: sha512-wANk6fBrUwdpY4isjWrKTufkrXdu1D2YHCot2fD/DfWxF5sMrVSA+KN7ydckvaTCh0HiqX9IVl0L5/ZoXg5M7w==
}
/@types/parse-json/4.0.0:
resolution:
{
integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
}
dev: true
/@types/pug/2.0.5:
resolution:
{
@@ -544,7 +559,7 @@ packages:
integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==
}
dependencies:
'@types/node': 17.0.23
'@types/node': 17.0.25
dev: false
/@types/sass/1.16.1:
@@ -553,7 +568,7 @@ packages:
integrity: sha512-iZUcRrGuz/Tbg3loODpW7vrQJkUtpY2fFSf4ELqqkApcS2TkZ1msk7ie8iZPB86lDOP8QOTTmuvWjc5S0R9OjQ==
}
dependencies:
'@types/node': 17.0.23
'@types/node': 17.0.25
dev: true
/@typescript-eslint/eslint-plugin/4.31.1_8ede7edd7694646e12d33c52460f622c:
@@ -1669,16 +1684,16 @@ packages:
ieee754: 1.2.1
dev: false
/bullmq/1.78.1:
/bullmq/1.80.4:
resolution:
{
integrity: sha512-er45mM8nGhgA83EVCJ4PNxPyDSzakvoxeFGU4vdSgYeB+SbeFQAlJYmAC50Ms7YFPstm1LeinbVZ+oX/BmBzOg==
integrity: sha512-j3PyjU16gqmb3Md9QjMInJdbMvxIlSJx7mojtoP06LV9MfhzW75DkDrpSuJlF0H+0+u6MViV4hhaGTxky5OJWw==
}
dependencies:
cron-parser: 4.2.1
get-port: 5.1.1
glob: 7.2.0
ioredis: 4.28.3
ioredis: 4.28.5
lodash: 4.17.21
msgpackr: 1.4.7
semver: 6.3.0
@@ -1928,6 +1943,14 @@ packages:
engines: { node: '>= 0.6' }
dev: false
/cookie/0.5.0:
resolution:
{
integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==
}
engines: { node: '>= 0.6' }
dev: false
/core-js/2.6.12:
resolution:
{
@@ -1937,20 +1960,6 @@ packages:
requiresBuild: true
dev: true
/cosmiconfig/7.0.1:
resolution:
{
integrity: sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==
}
engines: { node: '>=10' }
dependencies:
'@types/parse-json': 4.0.0
import-fresh: 3.3.0
parse-json: 5.2.0
path-type: 4.0.0
yaml: 1.10.2
dev: true
/cpu-features/0.0.2:
resolution:
{
@@ -2037,10 +2046,10 @@ packages:
}
dev: false
/dayjs/1.11.0:
/dayjs/1.11.1:
resolution:
{
integrity: sha512-JLC809s6Y948/FuCZPm5IX8rRhQwOiyMb2TfVVQEixG7P8Lm/gt5S7yoQZmC8x1UehI9Pb7sksEt4xx14m+7Ug==
integrity: sha512-ER7EjqVAMkRRsxNCC5YqJ9d9VQYuWdGt7aiH2qA5R5wt8ZmWaP2dLUSIK6y/kVzLMlmh1Tvu5xUf4M/wdGJ5KA==
}
dev: false
@@ -2296,15 +2305,6 @@ packages:
ansi-colors: 4.1.1
dev: true
/error-ex/1.3.2:
resolution:
{
integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
}
dependencies:
is-arrayish: 0.2.1
dev: true
/es6-promise/3.3.1:
resolution: { integrity: sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM= }
dev: true
@@ -3216,16 +3216,6 @@ packages:
engines: { node: '>= 4' }
dev: true
/import-cwd/3.0.0:
resolution:
{
integrity: sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==
}
engines: { node: '>=8' }
dependencies:
import-from: 3.0.0
dev: true
/import-fresh/3.3.0:
resolution:
{
@@ -3237,16 +3227,6 @@ packages:
resolve-from: 4.0.0
dev: true
/import-from/3.0.0:
resolution:
{
integrity: sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==
}
engines: { node: '>=8' }
dependencies:
resolve-from: 5.0.0
dev: true
/imurmurhash/0.1.4:
resolution: { integrity: sha1-khi5srkoojixPcT7a21XbyMUU+o= }
engines: { node: '>=0.8.19' }
@@ -3281,10 +3261,10 @@ packages:
loose-envify: 1.4.0
dev: true
/ioredis/4.28.3:
/ioredis/4.28.5:
resolution:
{
integrity: sha512-9JOWVgBnuSxpIgfpjc1OeY1OLmA4t2KOWWURTDRXky+eWO0LZhI33pQNT9gYxANUXfh5p/zYephYni6GPRsksQ==
integrity: sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==
}
engines: { node: '>=6' }
dependencies:
@@ -3303,10 +3283,6 @@ packages:
- supports-color
dev: false
/is-arrayish/0.2.1:
resolution: { integrity: sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= }
dev: true
/is-binary-path/2.1.0:
resolution:
{
@@ -3441,13 +3417,6 @@ packages:
}
dev: false
/json-parse-even-better-errors/2.3.1:
resolution:
{
integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
}
dev: true
/json-schema-traverse/0.4.1:
resolution:
{
@@ -3547,14 +3516,18 @@ packages:
engines: { node: '>=10' }
dev: true
/lines-and-columns/1.1.6:
resolution: { integrity: sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= }
dev: true
/lint-staged/12.3.7:
/lilconfig/2.0.5:
resolution:
{
integrity: sha512-/S4D726e2GIsDVWIk1XGvheCaDm1SJRQp8efamZFWJxQMVEbOwSysp7xb49Oo73KYCdy97mIWinhlxcoNqIfIQ==
integrity: sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==
}
engines: { node: '>=10' }
dev: true
/lint-staged/12.4.0:
resolution:
{
integrity: sha512-3X7MR0h9b7qf4iXf/1n7RlVAx+EzpAZXoCEMhVSpaBlgKDfH2ewf+QUm7BddFyq29v4dgPP+8+uYpWuSWx035A==
}
engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 }
hasBin: true
@@ -3966,10 +3939,10 @@ packages:
path-key: 3.1.1
dev: true
/object-hash/2.2.0:
/object-hash/3.0.0:
resolution:
{
integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==
integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==
}
engines: { node: '>= 6' }
dev: true
@@ -4067,19 +4040,6 @@ packages:
callsites: 3.1.0
dev: true
/parse-json/5.2.0:
resolution:
{
integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==
}
engines: { node: '>=8' }
dependencies:
'@babel/code-frame': 7.12.11
error-ex: 1.3.2
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.1.6
dev: true
/path-is-absolute/1.0.1:
resolution: { integrity: sha1-F0uSaHNVNP+8es5r9TpanhtcX18= }
engines: { node: '>=0.10.0' }
@@ -4143,21 +4103,24 @@ packages:
postcss: 8.4.12
dev: true
/postcss-load-config/3.1.0_ts-node@10.7.0:
/postcss-load-config/3.1.4_postcss@8.4.12+ts-node@10.7.0:
resolution:
{
integrity: sha512-ipM8Ds01ZUophjDTQYSVP70slFSYg3T0/zyfII5vzhN6V57YSxMgG5syXuwi5VtS8wSf3iL30v0uBdoIVx4Q0g==
integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==
}
engines: { node: '>= 10' }
peerDependencies:
postcss: '>=8.0.9'
ts-node: '>=9.0.0'
peerDependenciesMeta:
postcss:
optional: true
ts-node:
optional: true
dependencies:
import-cwd: 3.0.0
lilconfig: 2.0.4
ts-node: 10.7.0_ee885bc7281b682b6adbed6ae09ee090
lilconfig: 2.0.5
postcss: 8.4.12
ts-node: 10.7.0_de7c86b0cde507c63a0402da5b982bd3
yaml: 1.10.2
dev: true
@@ -4171,13 +4134,13 @@ packages:
postcss: ^8.2.14
dependencies:
postcss: 8.4.12
postcss-selector-parser: 6.0.9
postcss-selector-parser: 6.0.10
dev: true
/postcss-selector-parser/6.0.9:
/postcss-selector-parser/6.0.10:
resolution:
{
integrity: sha512-UO3SgnZOVTwu4kyLR22UQ1xZh086RyNZppb7lLAKBFK8a32ttG5i87Y/P3+2bRSjZNyJ1B7hfFNo273tKe9YxQ==
integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==
}
engines: { node: '>=4' }
dependencies:
@@ -4225,10 +4188,10 @@ packages:
svelte: 3.47.0
dev: true
/prettier-plugin-tailwindcss/0.1.8_prettier@2.6.2:
/prettier-plugin-tailwindcss/0.1.10_prettier@2.6.2:
resolution:
{
integrity: sha512-hwarSBCswAXa+kqYtaAkFr3Vop9o04WOyZs0qo3NyvW8L7f1rif61wRyq0+ArmVThOuRBcJF5hjGXYk86cwemg==
integrity: sha512-ooDGNuXUjgCXfShliVYQ6+0iXqUFXn+zdNInPe0WZN9qINt9srbLGFGY5jeVL4MXtY20/4S8JaBcd8l6N6NfCQ==
}
engines: { node: '>=12.17.0' }
peerDependencies:
@@ -4437,14 +4400,6 @@ packages:
engines: { node: '>=4' }
dev: true
/resolve-from/5.0.0:
resolution:
{
integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
}
engines: { node: '>=8' }
dev: true
/resolve/1.22.0:
resolution:
{
@@ -4903,10 +4858,10 @@ packages:
engines: { node: '>= 0.4' }
dev: true
/svelte-check/2.6.0_postcss@8.4.12+svelte@3.47.0:
/svelte-check/2.7.0_postcss@8.4.12+svelte@3.47.0:
resolution:
{
integrity: sha512-POL3IqLUuGqb9DdvuXQaSTNXYnw/odK4hqW86+2LwGcZTdbUPKBBln7pq74wXmcnRE+12bXMY1CvbcUNa2d5nw==
integrity: sha512-GrvG24j0+i8AOm0k0KyJ6Dqc+TAR2yzB7rtS4nljHStunVxCTr/1KYlv4EsOeoqtHLzeWMOd5D2O6nDdP/yw4A==
}
hasBin: true
peerDependencies:
@@ -4919,7 +4874,7 @@ packages:
sade: 1.7.4
source-map: 0.7.3
svelte: 3.47.0
svelte-preprocess: 4.10.5_41810887ae6c6d59323116f47e33fa38
svelte-preprocess: 4.10.6_41810887ae6c6d59323116f47e33fa38
typescript: 4.6.3
transitivePeerDependencies:
- '@babel/core'
@@ -4945,17 +4900,17 @@ packages:
svelte: 3.47.0
dev: true
/svelte-kit-cookie-session/2.1.2:
/svelte-kit-cookie-session/2.1.3:
resolution:
{
integrity: sha512-PfxIWDhiyYWu7iKlL0GHpmwDrdFh+rX/WmBzOuvctF25UqngIo9MCiegWBSBLE1RBwNs5UqaIeI8+vligmY07g==
integrity: sha512-7Xk3CNbpLAi1KodlsV5W5jULQ2NxQunaXtAYqAuzIEXIq2EwC4oDa25kdmHjNe33epV0t4r0WwxZOuSdJPsapg==
}
dev: false
/svelte-preprocess/4.10.5_41810887ae6c6d59323116f47e33fa38:
/svelte-preprocess/4.10.6_41810887ae6c6d59323116f47e33fa38:
resolution:
{
integrity: sha512-VKXPRScCzAZqeBZOGq4LLwtNrAu++mVn7XvQox3eFDV7Ciq0Lg70Q8QWjH9iXF7J+pMlXhPsSFwpCb2E+hoeyA==
integrity: sha512-I2SV1w/AveMvgIQlUF/ZOO3PYVnhxfcpNyGt8pxpUVhPfyfL/CZBkkw/KPfuFix5FJ9TnnNYMhACK3DtSaYVVQ==
}
engines: { node: '>= 9.11.2' }
requiresBuild: true
@@ -5022,6 +4977,19 @@ packages:
engines: { node: '>= 8' }
dev: true
/sveltekit-i18n/2.1.2_svelte@3.46.4:
resolution:
{
integrity: sha512-s5YxcbNd2EWNZaZR1A4Drt8s53E4fpUkN4XIWd3VRpw1pihZVWssqmBW1qkjQ6AB0kiu1Qwule+vt1HkbQOjrg==
}
peerDependencies:
svelte: ^3.x
dependencies:
'@sveltekit-i18n/base': 1.1.1_svelte@3.46.4
'@sveltekit-i18n/parser-default': 1.0.3
svelte: 3.46.4
dev: true
/table/6.7.2:
resolution:
{
@@ -5037,7 +5005,7 @@ packages:
strip-ansi: 6.0.1
dev: true
/tailwindcss-scrollbar/0.1.0_tailwindcss@3.0.23:
/tailwindcss-scrollbar/0.1.0_tailwindcss@3.0.24:
resolution:
{
integrity: sha512-egipxw4ooQDh94x02XQpPck0P0sfwazwoUGfA9SedPATIuYDR+6qe8d31Gl7YsSMRiOKDkkqfI0kBvEw9lT/Hg==
@@ -5045,38 +5013,35 @@ packages:
peerDependencies:
tailwindcss: '>= 2.x.x'
dependencies:
tailwindcss: 3.0.23_b89136460714832cdda11d1e9d57d1ff
tailwindcss: 3.0.24_ts-node@10.7.0
dev: false
/tailwindcss/3.0.23_b89136460714832cdda11d1e9d57d1ff:
/tailwindcss/3.0.24_ts-node@10.7.0:
resolution:
{
integrity: sha512-+OZOV9ubyQ6oI2BXEhzw4HrqvgcARY38xv3zKcjnWtMIZstEsXdI9xftd1iB7+RbOnj2HOEzkA0OyB5BaSxPQA==
integrity: sha512-H3uMmZNWzG6aqmg9q07ZIRNIawoiEcNFKDfL+YzOPuPsXuDXxJxB9icqzLgdzKNwjG3SAro2h9SYav8ewXNgig==
}
engines: { node: '>=12.13.0' }
hasBin: true
peerDependencies:
autoprefixer: ^10.0.2
dependencies:
arg: 5.0.1
autoprefixer: 10.4.4_postcss@8.4.12
chalk: 4.1.2
chokidar: 3.5.3
color-name: 1.1.4
cosmiconfig: 7.0.1
detective: 5.2.0
didyoumean: 1.2.2
dlv: 1.1.3
fast-glob: 3.2.11
glob-parent: 6.0.2
is-glob: 4.0.3
lilconfig: 2.0.5
normalize-path: 3.0.0
object-hash: 2.2.0
object-hash: 3.0.0
picocolors: 1.0.0
postcss: 8.4.12
postcss-js: 4.0.0_postcss@8.4.12
postcss-load-config: 3.1.0_ts-node@10.7.0
postcss-load-config: 3.1.4_postcss@8.4.12+ts-node@10.7.0
postcss-nested: 5.0.6_postcss@8.4.12
postcss-selector-parser: 6.0.9
postcss-selector-parser: 6.0.10
postcss-value-parser: 4.2.0
quick-lru: 5.1.1
resolve: 1.22.0
@@ -5148,7 +5113,7 @@ packages:
engines: { node: '>=0.10.0' }
dev: true
/ts-node/10.7.0_ee885bc7281b682b6adbed6ae09ee090:
/ts-node/10.7.0_de7c86b0cde507c63a0402da5b982bd3:
resolution:
{
integrity: sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==
@@ -5170,7 +5135,7 @@ packages:
'@tsconfig/node12': 1.0.9
'@tsconfig/node14': 1.0.1
'@tsconfig/node16': 1.0.2
'@types/node': 17.0.23
'@types/node': 17.0.25
acorn: 8.5.0
acorn-walk: 8.2.0
arg: 4.1.3

View File

@@ -0,0 +1,12 @@
-- CreateTable
CREATE TABLE "ServicePersistentStorage" (
"id" TEXT NOT NULL PRIMARY KEY,
"serviceId" TEXT NOT NULL,
"path" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "ServicePersistentStorage_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "ServicePersistentStorage_serviceId_path_key" ON "ServicePersistentStorage"("serviceId", "path");

View File

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

View File

@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "Application" ADD COLUMN "denoMainFile" TEXT;
ALTER TABLE "Application" ADD COLUMN "denoOptions" TEXT;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Build" ADD COLUMN "branch" TEXT;

View File

@@ -0,0 +1,17 @@
-- CreateTable
CREATE TABLE "Umami" (
"id" TEXT NOT NULL PRIMARY KEY,
"serviceId" TEXT NOT NULL,
"postgresqlUser" TEXT NOT NULL,
"postgresqlPassword" TEXT NOT NULL,
"postgresqlDatabase" TEXT NOT NULL,
"postgresqlPublicPort" INTEGER,
"umamiAdminPassword" TEXT NOT NULL,
"hashSalt" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Umami_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "Umami_serviceId_key" ON "Umami"("serviceId");

View File

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

View File

@@ -1,6 +1,6 @@
generator client {
provider = "prisma-client-js"
binaryTargets = ["linux-musl"]
provider = "prisma-client-js"
binaryTargets = ["native", "linux-musl"]
}
datasource db {
@@ -18,6 +18,7 @@ model Setting {
proxyPassword String
proxyUser String
proxyHash String?
isAutoUpdateEnabled Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
@@ -91,6 +92,9 @@ model Application {
pythonWSGI String?
pythonModule String?
pythonVariable String?
dockerFileLocation String?
denoMainFile String?
denoOptions String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
settings ApplicationSettings?
@@ -118,14 +122,25 @@ model ApplicationSettings {
model ApplicationPersistentStorage {
id String @id @default(cuid())
application Application @relation(fields: [applicationId], references: [id])
applicationId String
path String
applicationId String
path String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([applicationId, path])
}
model ServicePersistentStorage {
id String @id @default(cuid())
service Service @relation(fields: [serviceId], references: [id])
serviceId String
path String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([serviceId, path])
}
model Secret {
id String @id @default(cuid())
name String
@@ -169,6 +184,7 @@ model Build {
githubAppId String?
gitlabAppId String?
commit String?
branch String?
status String? @default("queued")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -267,17 +283,17 @@ model DatabaseSettings {
}
model Service {
id String @id @default(cuid())
id String @id @default(cuid())
name String
fqdn String?
dualCerts Boolean @default(false)
dualCerts Boolean @default(false)
type String?
version String?
teams Team[]
destinationDockerId String?
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
plausibleAnalytics PlausibleAnalytics?
minio Minio?
vscodeserver Vscodeserver?
@@ -285,6 +301,8 @@ model Service {
ghost Ghost?
serviceSecret ServiceSecret[]
meiliSearch MeiliSearch?
persistentStorage ServicePersistentStorage[]
umami Umami?
}
model PlausibleAnalytics {
@@ -369,3 +387,17 @@ model MeiliSearch {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Umami {
id String @id @default(cuid())
serviceId String @unique
postgresqlUser String
postgresqlPassword String
postgresqlDatabase String
postgresqlPublicPort Int?
umamiAdminPassword String
hashSalt String
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

View File

@@ -50,6 +50,20 @@ async function main() {
}
});
}
// Set auto-update based on env variable
const isAutoUpdateEnabled = process.env['COOLIFY_AUTO_UPDATE'] === 'true';
const settings = await prisma.setting.findFirst({});
if (settings) {
await prisma.setting.update({
where: {
id: settings.id
},
data: {
isAutoUpdateEnabled
}
});
}
}
main()
.catch((e) => {

7
src/app.d.ts vendored
View File

@@ -6,7 +6,11 @@ declare namespace App {
cookies: Record<string, string>;
}
interface Platform {}
interface Session extends SessionData {}
interface Session extends SessionData {
whiteLabelDetails: {
icon: string | null;
};
}
interface Stuff {
service: any;
application: any;
@@ -27,6 +31,7 @@ interface SessionData {
userId?: string | null;
teamId?: string | null;
permission?: string;
lang?: string;
isAdmin?: boolean;
expires?: string | null;
}

View File

@@ -6,8 +6,12 @@ import { getUserDetails, sentry } from '$lib/common';
import { version } from '$lib/common';
import cookie from 'cookie';
import { dev } from '$app/env';
import { locales } from '$lib/translations';
const whiteLabeled = process.env['COOLIFY_WHITE_LABELED'] === 'true';
const whiteLabelDetails = {
icon: (whiteLabeled && process.env['COOLIFY_WHITE_LABELED_ICON']) || null
};
export const handle = handleSession(
{
@@ -17,6 +21,24 @@ export const handle = handleSession(
},
async function ({ event, resolve }) {
let response;
const { url, request } = event;
// Get defined locales
const supportedLocales = locales.get();
let locale;
if (event.locals.cookies['lang']) {
locale = event.locals.cookies['lang'];
} else if (!locale) {
locale = `${`${request.headers.get('accept-language')}`.match(
/[a-zA-Z]+?(?=-|_|,|;)/
)}`.toLowerCase();
}
// Set default locale if user preferred locale does not match
if (!supportedLocales.includes(locale)) locale = 'en';
try {
if (event.locals.cookies) {
if (event.locals.cookies['kit.session']) {
@@ -36,12 +58,14 @@ export const handle = handleSession(
}
response = await resolve(event, {
ssr: !event.url.pathname.startsWith('/webhooks/success')
ssr: !event.url.pathname.startsWith('/webhooks/success'),
transformPage: ({ html }) => html.replace(/<html.*>/, `<html lang="${locale}">`)
});
} catch (error) {
console.log(error);
response = await resolve(event, {
ssr: !event.url.pathname.startsWith('/webhooks/success')
ssr: !event.url.pathname.startsWith('/webhooks/success'),
transformPage: ({ html }) => html.replace(/<html.*>/, `<html lang="${locale}">`)
});
response.headers.append(
'Set-Cookie',
@@ -64,16 +88,27 @@ export const handle = handleSession(
expires: new Date('Thu, 01 Jan 1970 00:00:01 GMT')
})
);
} finally {
return response;
}
response.headers.append(
'Set-Cookie',
cookie.serialize('lang', locale, {
path: '/',
sameSite: 'strict',
maxAge: 30 * 24 * 60 * 60
})
);
return response;
}
);
export const getSession: GetSession = function ({ locals }) {
return {
lang: locals.cookies.lang,
version,
whiteLabeled,
whiteLabelDetails,
...locals.session.data
};
};

View File

@@ -91,7 +91,9 @@ export const setDefaultConfiguration = async (data) => {
startCommand,
buildCommand,
publishDirectory,
baseDirectory
baseDirectory,
dockerFileLocation,
denoMainFile
} = data;
const template = scanningTemplates[buildPack];
if (!port) {
@@ -102,14 +104,25 @@ export const setDefaultConfiguration = async (data) => {
else if (buildPack === 'php') port = 80;
else if (buildPack === 'python') port = 8000;
}
if (!installCommand) installCommand = template?.installCommand || 'yarn install';
if (!startCommand) startCommand = template?.startCommand || 'yarn start';
if (!buildCommand) buildCommand = template?.buildCommand || null;
if (!installCommand && buildPack !== 'static')
installCommand = template?.installCommand || 'yarn install';
if (!startCommand && buildPack !== 'static')
startCommand = template?.startCommand || 'yarn start';
if (!buildCommand && buildPack !== 'static') buildCommand = template?.buildCommand || null;
if (!publishDirectory) publishDirectory = template?.publishDirectory || null;
if (baseDirectory) {
if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`;
if (!baseDirectory.endsWith('/')) baseDirectory = `${baseDirectory}/`;
}
if (dockerFileLocation) {
if (!dockerFileLocation.startsWith('/')) dockerFileLocation = `/${dockerFileLocation}`;
if (dockerFileLocation.endsWith('/')) dockerFileLocation = dockerFileLocation.slice(0, -1);
} else {
dockerFileLocation = '/Dockerfile';
}
if (!denoMainFile) {
denoMainFile = 'main.ts';
}
return {
buildPack,
@@ -118,7 +131,9 @@ export const setDefaultConfiguration = async (data) => {
startCommand,
buildCommand,
publishDirectory,
baseDirectory
baseDirectory,
dockerFileLocation,
denoMainFile
};
};
@@ -184,7 +199,11 @@ export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, ap
}
`
);
await saveBuildLog({ line: 'Copied default configuration file.', buildId, applicationId });
await saveBuildLog({
line: 'Copied default configuration file for Nginx.',
buildId,
applicationId
});
}
} catch (error) {
console.log(error);

View File

@@ -0,0 +1,54 @@
import { buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => {
const { workdir, port, baseDirectory, secrets, pullmergeRequestId, denoMainFile, denoOptions } =
data;
const Dockerfile: Array<string> = [];
let depsFound = false;
try {
await fs.readFile(`${workdir}${baseDirectory || ''}/deps.ts`);
depsFound = true;
} catch (error) {}
Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`);
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
}
}
});
}
if (depsFound) {
Dockerfile.push(`COPY .${baseDirectory || ''}/deps.ts /app`);
Dockerfile.push(`RUN deno cache deps.ts`);
}
Dockerfile.push(`COPY ${denoMainFile} /app`);
Dockerfile.push(`RUN deno cache ${denoMainFile}`);
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
Dockerfile.push(`ENV NO_COLOR true`);
Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push(`CMD deno run ${denoOptions ? denoOptions.split(' ') : ''} ${denoMainFile}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
};
export default async function (data) {
try {
const image = 'denoland/deno:latest';
await createDockerfile(data, image);
await buildImage(data);
} catch (error) {
throw error;
}
}

View File

@@ -10,15 +10,16 @@ export default async function ({
buildId,
baseDirectory,
secrets,
pullmergeRequestId
pullmergeRequestId,
dockerFileLocation
}) {
try {
let file = `${workdir}/Dockerfile`;
const file = `${workdir}${dockerFileLocation}`;
let dockerFileOut = `${workdir}`;
if (baseDirectory) {
file = `${workdir}/${baseDirectory}/Dockerfile`;
workdir = `${workdir}/${baseDirectory}`;
dockerFileOut = `${workdir}${baseDirectory}`;
workdir = `${workdir}${baseDirectory}`;
}
const Dockerfile: Array<string> = (await fs.readFile(`${file}`, 'utf8'))
.toString()
.trim()
@@ -41,8 +42,8 @@ export default async function ({
}
});
}
await fs.writeFile(`${file}`, Dockerfile.join('\n'));
await buildImage({ applicationId, tag, workdir, docker, buildId, debug });
await fs.writeFile(`${dockerFileOut}${dockerFileLocation}`, Dockerfile.join('\n'));
await buildImage({ applicationId, tag, workdir, docker, buildId, debug, dockerFileLocation });
} catch (error) {
throw error;
}

View File

@@ -13,6 +13,7 @@ import rust from './rust';
import astro from './static';
import eleventy from './static';
import python from './python';
import deno from './deno';
export {
node,
@@ -29,5 +30,6 @@ export {
rust,
astro,
eleventy,
python
python,
deno
};

View File

@@ -26,7 +26,7 @@ try {
initialScope: {
tags: {
appId: process.env['COOLIFY_APP_ID'],
'os.arch': os.arch(),
'os.arch': getOsArch(),
'os.platform': os.platform(),
'os.release': os.release()
}
@@ -61,14 +61,12 @@ export const saveBuildLog = async ({
buildId: string;
applicationId: string;
}): Promise<Job> => {
if (line) {
if (line.includes('ghs_')) {
const regex = /ghs_.*@/g;
line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
}
const addTimestamp = `${generateTimestamp()} ${line}`;
return await buildLogQueue.add(buildId, { buildId, line: addTimestamp, applicationId });
if (line && typeof line === 'string' && line.includes('ghs_')) {
const regex = /ghs_.*@/g;
line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
}
const addTimestamp = `${generateTimestamp()} ${line}`;
return await buildLogQueue.add(buildId, { buildId, line: addTimestamp, applicationId });
};
export const getTeam = (event: RequestEvent): string | null => {
@@ -93,11 +91,16 @@ export const getUserDetails = async (
}> => {
const teamId = getTeam(event);
const userId = event?.locals?.session?.data?.userId || null;
const { permission = 'read' } = await db.prisma.permission.findFirst({
where: { teamId, userId },
select: { permission: true },
rejectOnNotFound: true
});
let permission = 'read';
if (teamId && userId) {
const data = await db.prisma.permission.findFirst({
where: { teamId, userId },
select: { permission: true },
rejectOnNotFound: true
});
if (data.permission) permission = data.permission;
}
const payload = {
teamId,
userId,
@@ -172,3 +175,7 @@ export function generateTimestamp(): string {
export function getDomain(domain: string): string {
return domain?.replace('https://', '').replace('http://', '');
}
export function getOsArch() {
return os.arch();
}

View File

@@ -6,6 +6,7 @@
import N8n from './svg/services/N8n.svelte';
import NocoDb from './svg/services/NocoDB.svelte';
import PlausibleAnalytics from './svg/services/PlausibleAnalytics.svelte';
import Umami from './svg/services/Umami.svelte';
import UptimeKuma from './svg/services/UptimeKuma.svelte';
import VaultWarden from './svg/services/VaultWarden.svelte';
import VsCodeServer from './svg/services/VSCodeServer.svelte';
@@ -52,4 +53,8 @@
<a href="https://ghost.org" target="_blank">
<Ghost />
</a>
{:else if service.type === 'umami'}
<a href="https://umami.is" target="_blank">
<Umami />
</a>
{/if}

View File

@@ -19,7 +19,7 @@ export const staticDeployments = [
'astro',
'eleventy'
];
export const notNodeDeployments = ['php', 'docker', 'rust', 'python'];
export const notNodeDeployments = ['php', 'docker', 'rust', 'python', 'deno'];
export function getDomain(domain) {
return domain?.replace('https://', '').replace('http://', '');
@@ -180,5 +180,16 @@ export const supportedServiceTypesAndVersions = [
ports: {
main: 7700
}
},
{
name: 'umami',
fancyName: 'Umami',
baseImage: 'ghcr.io/mikecao/umami',
images: ['postgres:12-alpine'],
versions: ['postgresql-latest'],
recommendedVersion: 'postgresql-latest',
ports: {
main: 3000
}
}
];

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@@ -0,0 +1,85 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<svg
version="1.0"
xmlns="http://www.w3.org/2000/svg"
width="856.000000pt"
height="856.000000pt"
viewBox="0 0 856.000000 856.000000"
preserveAspectRatio="xMidYMid meet"
class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 mx-auto'}
>
<metadata> Created by potrace 1.11, written by Peter Selinger 2001-2013 </metadata>
<g
transform="translate(0.000000,856.000000) scale(0.100000,-0.100000)"
fill="currentColor"
stroke="none"
>
<path
d="M4027 8163 c-2 -2 -28 -5 -58 -7 -50 -4 -94 -9 -179 -22 -19 -2 -48
-6 -65 -9 -47 -6 -236 -44 -280 -55 -22 -6 -49 -12 -60 -15 -34 -6 -58 -13
-130 -36 -38 -13 -72 -23 -75 -24 -29 -6 -194 -66 -264 -96 -49 -22 -95 -39
-102 -39 -7 0 -19 -7 -28 -15 -8 -9 -18 -15 -21 -14 -7 1 -197 -92 -205 -101
-3 -3 -21 -13 -40 -24 -79 -42 -123 -69 -226 -137 -94 -62 -246 -173 -280
-204 -6 -5 -29 -25 -52 -43 -136 -111 -329 -305 -457 -462 -21 -25 -41 -47
-44 -50 -4 -3 -22 -26 -39 -52 -18 -25 -38 -52 -45 -60 -34 -35 -207 -308
-259 -408 -13 -25 -25 -47 -28 -50 -11 -11 -121 -250 -159 -346 -42 -105 -114
-321 -126 -374 l-7 -30 -263 0 c-245 0 -268 -2 -321 -21 -94 -35 -171 -122
-191 -216 -9 -39 -8 -852 0 -938 9 -87 16 -150 23 -195 3 -19 6 -48 8 -65 3
-29 14 -97 22 -140 3 -11 7 -36 10 -55 3 -19 9 -51 14 -70 5 -19 11 -46 14
-60 29 -138 104 -401 145 -505 5 -11 23 -58 42 -105 18 -47 42 -105 52 -130
11 -25 21 -49 22 -55 3 -10 109 -224 164 -330 18 -33 50 -89 71 -124 22 -34
40 -64 40 -66 0 -8 104 -161 114 -167 6 -4 7 -8 3 -8 -4 0 4 -12 18 -27 14
-15 25 -32 25 -36 0 -5 6 -14 13 -21 6 -7 21 -25 32 -41 11 -15 34 -44 50 -64
17 -21 41 -52 55 -70 13 -18 33 -43 45 -56 11 -13 42 -49 70 -81 100 -118 359
-369 483 -469 34 -27 62 -53 62 -57 0 -5 6 -8 13 -8 7 0 19 -9 27 -20 8 -11
19 -20 26 -20 6 0 19 -9 29 -20 10 -11 22 -20 27 -20 5 0 23 -13 41 -30 18
-16 37 -30 44 -30 6 0 13 -4 15 -8 3 -8 186 -132 194 -132 2 0 27 -15 56 -34
132 -83 377 -207 558 -280 36 -15 74 -31 85 -36 62 -26 220 -81 320 -109 79
-23 191 -53 214 -57 14 -3 28 -7 31 -9 4 -2 20 -7 36 -9 16 -3 40 -8 54 -11
14 -3 36 -8 50 -11 14 -2 36 -7 50 -10 13 -3 40 -8 60 -10 19 -2 46 -7 60 -10
54 -10 171 -25 320 -40 90 -9 613 -12 636 -4 11 5 28 4 37 -1 9 -6 17 -6 17
-1 0 4 10 8 23 9 29 0 154 12 192 18 17 3 46 7 65 9 70 10 131 20 183 32 16 3
38 7 50 9 45 7 165 36 252 60 50 14 100 28 112 30 12 3 34 10 48 15 14 5 25 7
25 4 0 -4 6 -2 13 3 6 6 30 16 52 22 22 7 47 15 55 18 8 4 17 7 20 7 10 2 179
68 240 94 96 40 342 159 395 191 17 10 53 30 80 46 28 15 81 47 118 71 37 24
72 44 76 44 5 0 11 3 13 8 2 4 30 25 63 47 33 22 62 42 65 45 3 3 50 38 105
79 55 40 105 79 110 85 6 6 24 22 40 34 85 65 465 430 465 447 0 3 8 13 18 23
9 10 35 40 57 66 22 27 47 56 55 65 8 9 42 52 74 96 32 44 71 96 85 115 140
183 358 576 461 830 12 30 28 69 36 85 24 56 123 355 117 355 -3 0 -1 6 5 13
6 6 14 30 18 52 10 48 9 46 17 65 5 13 37 155 52 230 9 42 35 195 40 231 34
235 40 357 40 804 l0 420 -24 44 c-46 87 -143 157 -231 166 -19 2 -144 4 -276
4 l-242 1 -36 118 c-21 64 -46 139 -56 166 -11 27 -20 52 -20 57 0 5 -11 33
-25 63 -14 30 -25 58 -25 61 0 18 -152 329 -162 333 -5 2 -8 10 -8 18 0 8 -4
14 -10 14 -5 0 -9 3 -8 8 3 9 -40 82 -128 217 -63 97 -98 145 -187 259 -133
171 -380 420 -559 564 -71 56 -132 102 -138 102 -5 0 -10 3 -10 8 0 4 -25 23
-55 42 -30 19 -55 38 -55 43 0 4 -6 7 -13 7 -7 0 -22 8 -33 18 -11 9 -37 26
-59 37 -21 11 -44 25 -50 30 -41 37 -413 220 -540 266 -27 9 -61 22 -75 27
-14 5 -28 10 -32 11 -4 1 -28 10 -53 21 -25 11 -46 19 -48 18 -2 -1 -109 29
-137 40 -13 4 -32 9 -65 16 -5 1 -16 5 -22 9 -7 5 -13 6 -13 3 0 -2 -15 0 -32
5 -18 5 -44 11 -58 14 -14 3 -36 7 -50 10 -14 3 -50 9 -80 15 -30 6 -64 12
-75 14 -11 2 -45 6 -75 10 -30 4 -71 9 -90 12 -19 3 -53 6 -75 7 -22 1 -44 5
-50 8 -11 7 -542 9 -548 2z m57 -404 c7 10 436 8 511 -3 22 -3 60 -8 85 -11
25 -2 56 -6 70 -9 14 -2 43 -7 65 -10 38 -5 58 -9 115 -21 14 -3 34 -7 45 -9
11 -2 58 -14 105 -26 47 -12 92 -23 100 -25 35 -7 279 -94 308 -109 17 -9 34
-16 37 -16 3 1 20 -6 38 -14 17 -8 68 -31 112 -51 44 -20 82 -35 84 -35 2 1 7
-3 10 -8 3 -5 43 -28 88 -51 45 -23 87 -48 93 -56 7 -8 17 -15 22 -15 12 0
192 -121 196 -132 2 -4 8 -8 13 -8 10 0 119 -86 220 -172 102 -87 256 -244
349 -357 25 -30 53 -63 63 -73 9 -10 17 -22 17 -28 0 -5 3 -10 8 -10 4 0 25
-27 46 -60 22 -33 43 -60 48 -60 4 0 8 -5 8 -11 0 -6 11 -25 25 -43 14 -18 25
-38 25 -44 0 -7 4 -12 8 -12 5 0 16 -15 25 -32 9 -18 30 -55 47 -83 46 -77
161 -305 154 -305 -4 0 -2 -6 4 -12 6 -7 23 -47 40 -88 16 -41 33 -84 37 -95
5 -11 9 -22 10 -25 0 -3 11 -36 24 -73 13 -38 21 -70 19 -73 -3 -2 -1386 -3
-3075 -2 l-3071 3 38 110 c47 137 117 301 182 425 62 118 167 295 191 320 9
11 17 22 17 25 0 7 39 63 58 83 6 7 26 35 44 60 18 26 37 52 43 57 6 6 34 37
61 70 48 59 271 286 329 335 17 14 53 43 80 65 28 22 52 42 55 45 3 3 21 17
40 30 19 14 40 28 45 32 40 32 105 78 109 78 3 0 28 16 55 35 26 19 53 35 58
35 5 0 18 8 29 18 17 15 53 35 216 119 118 60 412 176 422 166 3 -4 6 -2 6 4
0 6 12 13 28 16 15 3 52 12 82 21 30 9 63 19 73 21 10 2 27 7 37 10 10 3 29 8
42 10 13 3 48 10 78 16 30 7 61 12 68 12 6 0 12 4 12 9 0 5 5 6 10 3 6 -4 34
-2 63 4 51 11 71 13 197 26 36 4 67 9 69 11 2 2 10 -1 17 -7 8 -6 14 -7 18 0z"
/>
</g>
</svg>

View File

@@ -153,6 +153,16 @@ export function findBuildPack(pack, packageManager = 'npm') {
port: 8000
};
}
if (pack === 'deno') {
return {
...metaData,
installCommand: null,
buildCommand: null,
startCommand: null,
publishDirectory: null,
port: 8000
};
}
return {
name: 'node',
fancyName: 'Node.js',
@@ -262,6 +272,12 @@ export const buildPacks = [
fancyName: 'Python',
hoverColor: 'hover:bg-green-700',
color: 'bg-green-700'
},
{
name: 'deno',
fancyName: 'Deno',
hoverColor: 'hover:bg-green-700',
color: 'bg-green-700'
}
];
export const scanningTemplates = {

View File

@@ -263,7 +263,10 @@ export async function configureApplication({
publishDirectory,
pythonWSGI,
pythonModule,
pythonVariable
pythonVariable,
dockerFileLocation,
denoMainFile,
denoOptions
}: {
id: string;
buildPack: string;
@@ -278,6 +281,9 @@ export async function configureApplication({
pythonWSGI: string;
pythonModule: string;
pythonVariable: string;
dockerFileLocation: string;
denoMainFile: string;
denoOptions: string;
}): Promise<Application> {
return await prisma.application.update({
where: { id },
@@ -293,7 +299,10 @@ export async function configureApplication({
publishDirectory,
pythonWSGI,
pythonModule,
pythonVariable
pythonVariable,
dockerFileLocation,
denoMainFile,
denoOptions
}
});
}

View File

@@ -9,6 +9,7 @@ import { default as ProdPrisma } from '@prisma/client';
import type { Database, DatabaseSettings } from '@prisma/client';
import generator from 'generate-password';
import forge from 'node-forge';
import getPort, { portNumbers } from 'get-port';
export function generatePassword(length = 24): string {
return generator.generate({
@@ -153,6 +154,7 @@ export function generateDatabaseConfiguration(database: Database & { settings: D
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
POSTGRESQL_POSTGRES_PASSWORD: string;
POSTGRESQL_USERNAME: string;
POSTGRESQL_PASSWORD: string;
POSTGRESQL_DATABASE: string;
@@ -252,3 +254,29 @@ export function generateDatabaseConfiguration(database: Database & { settings: D
};
}
}
export async function getFreePort() {
const data = await prisma.setting.findFirst();
const { minPort, maxPort } = data;
const dbUsed = await (
await prisma.database.findMany({
where: { publicPort: { not: null } },
select: { publicPort: true }
})
).map((a) => a.publicPort);
const wpFtpUsed = await (
await prisma.wordpress.findMany({
where: { ftpPublicPort: { not: null } },
select: { ftpPublicPort: true }
})
).map((a) => a.ftpPublicPort);
const wpUsed = await (
await prisma.wordpress.findMany({
where: { mysqlPublicPort: { not: null } },
select: { mysqlPublicPort: true }
})
).map((a) => a.mysqlPublicPort);
const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed];
return await getPort({ port: portNumbers(minPort, maxPort), exclude: usedPorts });
}

View File

@@ -90,7 +90,7 @@ export async function addGitLabSource({
appSecret,
groupName
}) {
const encrptedAppSecret = encrypt(appSecret);
const encryptedAppSecret = encrypt(appSecret);
await prisma.gitSource.update({ where: { id }, data: { type, apiUrl, htmlUrl, name } });
return await prisma.gitlabApp.create({
data: {

View File

@@ -1,9 +1,27 @@
import { decrypt, encrypt } from '$lib/crypto';
import type { Minio, Service } from '@prisma/client';
import type { Minio, Prisma, Service } from '@prisma/client';
import cuid from 'cuid';
import { generatePassword } from '.';
import { prisma } from './common';
const include: Prisma.ServiceInclude = {
destinationDocker: true,
persistentStorage: true,
serviceSecret: true,
minio: true,
plausibleAnalytics: true,
vscodeserver: true,
wordpress: true,
ghost: true,
meiliSearch: true,
umami: true
};
export async function listServicesWithIncludes() {
return await prisma.service.findMany({
include,
orderBy: { createdAt: 'desc' }
});
}
export async function listServices(teamId: string): Promise<Service[]> {
if (teamId === '0') {
return await prisma.service.findMany({ include: { teams: true } });
@@ -27,16 +45,6 @@ export async function newService({
export async function getService({ id, teamId }: { id: string; teamId: string }): Promise<Service> {
let body;
const include = {
destinationDocker: true,
plausibleAnalytics: true,
minio: true,
vscodeserver: true,
wordpress: true,
ghost: true,
serviceSecret: true,
meiliSearch: true
};
if (teamId === '0') {
body = await prisma.service.findFirst({
where: { id },
@@ -49,6 +57,12 @@ export async function getService({ id, teamId }: { id: string; teamId: string })
});
}
if (body?.serviceSecret.length > 0) {
body.serviceSecret = body.serviceSecret.map((s) => {
s.value = decrypt(s.value);
return s;
});
}
if (body.plausibleAnalytics?.postgresqlPassword)
body.plausibleAnalytics.postgresqlPassword = decrypt(
body.plausibleAnalytics.postgresqlPassword
@@ -75,15 +89,14 @@ export async function getService({ id, teamId }: { id: string; teamId: string })
if (body.meiliSearch?.masterKey) body.meiliSearch.masterKey = decrypt(body.meiliSearch.masterKey);
if (body?.serviceSecret.length > 0) {
body.serviceSecret = body.serviceSecret.map((s) => {
s.value = decrypt(s.value);
return s;
});
}
if (body.wordpress?.ftpPassword) {
body.wordpress.ftpPassword = decrypt(body.wordpress.ftpPassword);
}
if (body.wordpress?.ftpPassword) body.wordpress.ftpPassword = decrypt(body.wordpress.ftpPassword);
if (body.umami?.postgresqlPassword)
body.umami.postgresqlPassword = decrypt(body.umami.postgresqlPassword);
if (body.umami?.umamiAdminPassword)
body.umami.umamiAdminPassword = decrypt(body.umami.umamiAdminPassword);
if (body.umami?.hashSalt) body.umami.hashSalt = decrypt(body.umami.hashSalt);
const settings = await prisma.setting.findFirst();
return { ...body, settings };
@@ -209,6 +222,27 @@ export async function configureServiceType({
meiliSearch: { create: { masterKey } }
}
});
} else if (type === 'umami') {
const umamiAdminPassword = encrypt(generatePassword());
const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword());
const postgresqlDatabase = 'umami';
const hashSalt = encrypt(generatePassword(64));
await prisma.service.update({
where: { id },
data: {
type,
umami: {
create: {
umamiAdminPassword,
postgresqlDatabase,
postgresqlPassword,
postgresqlUser,
hashSalt
}
}
}
});
}
}
@@ -362,8 +396,10 @@ export async function updateGhostService({
}
export async function removeService({ id }: { id: string }): Promise<void> {
await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id } });
await prisma.meiliSearch.deleteMany({ where: { serviceId: id } });
await prisma.ghost.deleteMany({ where: { serviceId: id } });
await prisma.umami.deleteMany({ where: { serviceId: id } });
await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } });
await prisma.minio.deleteMany({ where: { serviceId: id } });
await prisma.vscodeserver.deleteMany({ where: { serviceId: id } });

View File

@@ -46,8 +46,12 @@ export async function login({
if (users === 0) {
await prisma.setting.update({ where: { id }, data: { isRegistrationEnabled: false } });
// Create default network & start Coolify Proxy
await asyncExecShell(`docker network create --attachable coolify`);
await startCoolifyProxy('/var/run/docker.sock');
try {
await asyncExecShell(`docker network create --attachable coolify`);
} catch (error) {}
try {
await startCoolifyProxy('/var/run/docker.sock');
} catch (error) {}
uid = '0';
}

View File

@@ -85,7 +85,8 @@ export async function buildImage({
docker,
buildId,
isCache = false,
debug = false
debug = false,
dockerFileLocation = '/Dockerfile'
}) {
if (isCache) {
await saveBuildLog({ line: `Building cache image started.`, buildId, applicationId });
@@ -103,11 +104,12 @@ export async function buildImage({
const stream = await docker.engine.buildImage(
{ src: ['.'], context: workdir },
{
dockerfile: isCache ? 'Dockerfile-cache' : 'Dockerfile',
dockerfile: isCache ? `${dockerFileLocation}-cache` : dockerFileLocation,
t: `${applicationId}:${tag}${isCache ? '-cache' : ''}`
}
);
await streamEvents({ stream, docker, buildId, applicationId, debug });
await saveBuildLog({ line: `Building image successful!`, buildId, applicationId });
}
export function dockerInstance({ destinationDocker }): { engine: Dockerode; network: string } {

View File

@@ -6,6 +6,7 @@ import crypto from 'crypto';
import { checkContainer, checkHAProxy } from '.';
import { asyncExecShell, getDomain, getEngine } from '$lib/common';
import { supportedServiceTypesAndVersions } from '$lib/components/common';
import { listServicesWithIncludes } from '$lib/database';
const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555';
@@ -208,16 +209,7 @@ export async function configureHAProxy(): Promise<void> {
}
}
}
const services = await db.prisma.service.findMany({
include: {
destinationDocker: true,
minio: true,
plausibleAnalytics: true,
vscodeserver: true,
wordpress: true,
ghost: true
}
});
const services = await listServicesWithIncludes();
for (const service of services) {
const { fqdn, id, type, destinationDocker, destinationDockerId, updatedAt } = service;

View File

@@ -126,11 +126,11 @@ export async function startTcpProxy(
const host = getEngine(engine);
const containerName = `haproxy-for-${publicPort}`;
const found = await checkContainer(engine, containerName);
const foundDB = await checkContainer(engine, id);
const found = await checkContainer(engine, containerName, true);
const foundDependentContainer = await checkContainer(engine, id, true);
try {
if (foundDB && !found) {
if (foundDependentContainer && !found) {
const { stdout: Config } = await asyncExecShell(
`DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'`
);
@@ -141,6 +141,11 @@ export async function startTcpProxy(
} -d coollabsio/${defaultProxyImageTcp}`
);
}
if (!foundDependentContainer && found) {
return await asyncExecShell(
`DOCKER_HOST=${host} docker stop -t 0 ${containerName} && docker rm ${containerName}`
);
}
} catch (error) {
return error;
}
@@ -156,11 +161,11 @@ export async function startHttpProxy(
const host = getEngine(engine);
const containerName = `haproxy-for-${publicPort}`;
const found = await checkContainer(engine, containerName);
const foundDB = await checkContainer(engine, id);
const found = await checkContainer(engine, containerName, true);
const foundDependentContainer = await checkContainer(engine, id, true);
try {
if (foundDB && !found) {
if (foundDependentContainer && !found) {
const { stdout: Config } = await asyncExecShell(
`DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'`
);
@@ -169,6 +174,11 @@ export async function startHttpProxy(
`DOCKER_HOST=${host} docker run --restart always -e PORT=${publicPort} -e APP=${id} -e PRIVATE_PORT=${privatePort} --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' --network ${network} -p ${publicPort}:${publicPort} --name ${containerName} -d coollabsio/${defaultProxyImageHttp}`
);
}
if (!foundDependentContainer && found) {
return await asyncExecShell(
`DOCKER_HOST=${host} docker stop -t 0 ${containerName} && docker rm ${containerName}`
);
}
} catch (error) {
return error;
}
@@ -176,7 +186,7 @@ export async function startHttpProxy(
export async function startCoolifyProxy(engine: string): Promise<void> {
const host = getEngine(engine);
const found = await checkContainer(engine, 'coolify-haproxy');
const found = await checkContainer(engine, 'coolify-haproxy', true);
const { proxyPassword, proxyUser, id } = await db.listSettings();
if (!found) {
const { stdout: Config } = await asyncExecShell(
@@ -191,7 +201,25 @@ export async function startCoolifyProxy(engine: string): Promise<void> {
await configureNetworkCoolifyProxy(engine);
}
export async function checkContainer(engine: string, container: string): Promise<boolean> {
export async function isContainerExited(engine: string, containerName: string): Promise<boolean> {
let isExited = false;
const host = getEngine(engine);
try {
const { stdout } = await asyncExecShell(
`DOCKER_HOST="${host}" docker inspect -f '{{.State.Status}}' ${containerName}`
);
if (stdout.trim() === 'exited') {
isExited = true;
}
} catch (error) {}
return isExited;
}
export async function checkContainer(
engine: string,
container: string,
remove: boolean = false
): Promise<boolean> {
const host = getEngine(engine);
let containerFound = false;
@@ -202,7 +230,10 @@ export async function checkContainer(engine: string, container: string): Promise
const parsedStdout = JSON.parse(stdout);
const status = parsedStdout.Status;
const isRunning = status === 'running';
if (status === 'exited' || status === 'created') {
if (status === 'created') {
await asyncExecShell(`DOCKER_HOST="${host}" docker rm ${container}`);
}
if (remove && status === 'exited') {
await asyncExecShell(`DOCKER_HOST="${host}" docker rm ${container}`);
}
if (isRunning) {
@@ -236,9 +267,15 @@ export async function stopCoolifyProxy(
export async function configureNetworkCoolifyProxy(engine: string): Promise<void> {
const host = getEngine(engine);
const destinations = await db.prisma.destinationDocker.findMany({ where: { engine } });
const { stdout: networks } = await asyncExecShell(
`DOCKER_HOST="${host}" docker ps -a --filter name=coolify-haproxy --format '{{json .Networks}}'`
);
const configuredNetworks = networks.replace(/"/g, '').replace('\n', '').split(',');
for (const destination of destinations) {
await asyncExecShell(
`DOCKER_HOST="${host}" docker network connect ${destination.network} coolify-haproxy`
);
if (!configuredNetworks.includes(destination.network)) {
await asyncExecShell(
`DOCKER_HOST="${host}" docker network connect ${destination.network} coolify-haproxy`
);
}
}
}

4
src/lib/lang.json Normal file
View File

@@ -0,0 +1,4 @@
{
"fr": "Français",
"en": "English"
}

View File

@@ -6,9 +6,14 @@ import cuid from 'cuid';
import fs from 'fs/promises';
import getPort, { portNumbers } from 'get-port';
import { supportedServiceTypesAndVersions } from '$lib/components/common';
import { promises as dns } from 'dns';
import { listServicesWithIncludes } from '$lib/database';
export async function letsEncrypt(domain: string, id?: string, isCoolify = false): Promise<void> {
try {
const certbotImage =
process.arch === 'x64' ? 'certbot/certbot' : 'certbot/certbot:arm64v8-latest';
const data = await db.prisma.setting.findFirst();
const { minPort, maxPort } = data;
@@ -62,7 +67,7 @@ export async function letsEncrypt(domain: string, id?: string, isCoolify = false
if (found) return;
await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:${randomPort} -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port ${randomPort} -d ${nakedDomain} -d ${wwwDomain} --expand --agree-tos --non-interactive --register-unsafely-without-email ${
`DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:${randomPort} -v "coolify-letsencrypt:/etc/letsencrypt" ${certbotImage} --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port ${randomPort} -d ${nakedDomain} -d ${wwwDomain} --expand --agree-tos --non-interactive --register-unsafely-without-email ${
dev ? '--test-cert' : ''
}`
);
@@ -82,7 +87,7 @@ export async function letsEncrypt(domain: string, id?: string, isCoolify = false
}
if (found) return;
await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:${randomPort} -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port ${randomPort} -d ${domain} --expand --agree-tos --non-interactive --register-unsafely-without-email ${
`DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:${randomPort} -v "coolify-letsencrypt:/etc/letsencrypt" ${certbotImage} --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port ${randomPort} -d ${domain} --expand --agree-tos --non-interactive --register-unsafely-without-email ${
dev ? '--test-cert' : ''
}`
);
@@ -141,17 +146,7 @@ export async function generateSSLCerts(): Promise<void> {
console.log(`Error during generateSSLCerts with ${application.fqdn}: ${error}`);
}
}
const services = await db.prisma.service.findMany({
include: {
destinationDocker: true,
minio: true,
plausibleAnalytics: true,
vscodeserver: true,
wordpress: true,
ghost: true
},
orderBy: { createdAt: 'desc' }
});
const services = await listServicesWithIncludes();
for (const service of services) {
try {
@@ -198,16 +193,44 @@ export async function generateSSLCerts(): Promise<void> {
file.endsWith('.pem') && certificates.push(file.replace(/\.pem$/, ''));
}
}
const resolver = new dns.Resolver({ timeout: 2000 });
resolver.setServers(['8.8.8.8', '1.1.1.1']);
let ipv4, ipv6;
try {
ipv4 = await (await asyncExecShell(`curl -4s https://ifconfig.io`)).stdout;
} catch (error) {}
try {
ipv6 = await (await asyncExecShell(`curl -6s https://ifconfig.io`)).stdout;
} catch (error) {}
for (const ssl of ssls) {
if (!dev) {
if (
certificates.includes(ssl.domain) ||
certificates.includes(ssl.domain.replace('www.', ''))
) {
console.log(`Certificate for ${ssl.domain} already exists`);
// console.log(`Certificate for ${ssl.domain} already exists`);
} else {
console.log('Generating SSL for', ssl.domain);
await letsEncrypt(ssl.domain, ssl.id, ssl.isCoolify);
// Checking DNS entry before generating certificate
if (ipv4 || ipv6) {
let domains4 = [];
let domains6 = [];
try {
domains4 = await resolver.resolve4(ssl.domain);
} catch (error) {}
try {
domains6 = await resolver.resolve6(ssl.domain);
} catch (error) {}
if (domains4.length > 0 || domains6.length > 0) {
if (
(ipv4 && domains4.includes(ipv4.replace('\n', ''))) ||
(ipv6 && domains6.includes(ipv6.replace('\n', '')))
) {
console.log('Generating SSL for', ssl.domain);
return await letsEncrypt(ssl.domain, ssl.id, ssl.isCoolify);
}
}
}
console.log('DNS settings is incorrect for', ssl.domain, 'skipping.');
}
} else {
if (
@@ -216,7 +239,27 @@ export async function generateSSLCerts(): Promise<void> {
) {
console.log(`Certificate for ${ssl.domain} already exists`);
} else {
console.log('Generating SSL for', ssl.domain);
// Checking DNS entry before generating certificate
if (ipv4 || ipv6) {
let domains4 = [];
let domains6 = [];
try {
domains4 = await resolver.resolve4(ssl.domain);
} catch (error) {}
try {
domains6 = await resolver.resolve6(ssl.domain);
} catch (error) {}
if (domains4.length > 0 || domains6.length > 0) {
if (
(ipv4 && domains4.includes(ipv4.replace('\n', ''))) ||
(ipv6 && domains6.includes(ipv6.replace('\n', '')))
) {
console.log('Generating SSL for', ssl.domain);
return;
}
}
}
console.log('DNS settings is incorrect for', ssl.domain, 'skipping.');
}
}
}

329
src/lib/locales/en.json Normal file
View File

@@ -0,0 +1,329 @@
{
"layout": {
"update_done": "Update completed.",
"wait_new_version_startup": "Waiting for the new version to start...",
"new_version": "New version reachable. Reloading...",
"switch_to_a_different_team": "Switch to a different team...",
"update_available": "Update available"
},
"error": {
"you_can_find_your_way_back": "You can find your way back",
"here": "here",
"you_are_lost": "Ooops you are lost! But don't be afraid!"
},
"index": {
"dashboard": "Dashboard",
"applications": "Applications",
"destinations": "Destinations",
"git_sources": "Git Sources",
"databases": "Databases",
"services": "Services",
"teams": "Teams",
"not_implemented_yet": "Not implemented yet",
"database": "Database",
"settings": "Settings",
"global_settings": "Global Settings",
"secret": "Secret",
"team": "Team",
"logout": "Logout"
},
"login": {
"already_logged_in": "Already logged in...",
"authenticating": "Authenticating...",
"login": "Login"
},
"forms": {
"password": "Password",
"email": "Email address",
"passwords_not_match": "Passwords do not match.",
"password_again": "Password again",
"save": "Save",
"saving": "Saving...",
"name": "Name",
"value": "Value",
"action": "Action",
"is_required": "is required.",
"add": "Add",
"set": "Set",
"remove": "Remove",
"path": "Path",
"confirm_continue": "Are you sure to continue?",
"must_be_stopped_to_modify": "Must be stopped to modify.",
"port": "Port",
"default": "default",
"base_directory": "Base Directory",
"publish_directory": "Publish Directory",
"generated_automatically_after_start": "Generated automatically after start",
"roots_password": "Root's Password",
"root_user": "Root User",
"eg": "eg",
"user": "User",
"loading": "Loading...",
"version": "Version",
"host": "Host",
"already_used_for": "<span class=\"text-red-500\">{{type}}</span> already used for",
"configuration": "Configuration",
"engine": "Engine",
"network": "Network",
"ip_address": "IP Address",
"ssh_private_key": "SSH Private Key",
"type": "Type",
"html_url": "HTML URL",
"api_url": "API URL",
"organization": "Organization",
"new_password": "New password",
"super_secure_new_password": "Super secure new password",
"submit": "Submit",
"default_email_address": "Default Email Address",
"default_password": "Default Password",
"username": "Username",
"root_db_user": "Root DB User",
"root_db_password": "Root DB Password",
"api_port": "API Port",
"verifying": "Verifying",
"verify_emails_without_smtp": "Verify emails without SMTP",
"extra_config": "Extra Config",
"select_a_service": "Select a Service",
"select_a_service_version": "Select a Service version",
"removing": "Removing...",
"remove_domain": "Remove domain",
"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-yellow-500 font-bold'>9000-9100</span>",
"no_actions_available": "No actions available",
"admin_api_key": "Admin API key"
},
"register": {
"register": "Register",
"registering": "Registering...",
"first_user": "You are registering the first user. It will be the administrator of your Coolify instance."
},
"reset": {
"reset_password": "Reset",
"invalid_secret_key": "Invalid secret key.",
"secret_key": "Secret Key",
"find_path_secret_key": "You can find it in ~/coolify/.env (COOLIFY_SECRET_KEY)"
},
"application": {
"configuration": {
"buildpack": {
"choose_this_one": "Choose this one..."
},
"branch_already_in_use": "This branch is already used by another application. Webhooks won't work in this case for both applications. Are you sure you want to use it?",
"no_repositories_configured": "No repositories configured for your Git Application.",
"configure_it_now": "Configure it now",
"loading_repositories": "Loading repositories ...",
"select_a_repository": "Please select a repository",
"loading_branches": "Loading branches ...",
"select_a_repository_first": "Please select a repository first",
"select_a_branch": "Please select a branch",
"loading_groups": "Loading groups...",
"select_a_group": "Please select a group",
"loading_projects": "Loading projects...",
"select_a_project": "Please select a project",
"no_projects_found": "No projects found",
"no_branches_found": "No branches found",
"configure_build_pack": "Configure Build Pack",
"scanning_repository_suggest_build_pack": "Scanning repository to suggest a build pack for you...",
"found_lock_file": "Found lock file for <span class=\"font-bold text-orange-500 pl-1\">{{packageManager}}</span>. Using it for predefined commands commands.",
"configure_destination": "Configure Destination",
"no_configurable_destination": "No configurable Destination found",
"select_a_repository_project": "Select a Repository / Project",
"select_a_git_source": "Select a Git Source",
"no_configurable_git": "No configurable Git Source found",
"configuration_missing": "Configuration missing"
},
"build": {
"queued_waiting_exec": "Queued and waiting for execution.",
"build_logs_of": "Build logs of",
"running": "Running",
"queued": "Queued",
"finished_in": "Finished in",
"load_more": "Load More",
"no_logs": "No logs found",
"waiting_logs": "Waiting for the logs..."
},
"preview": {
"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.",
"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.",
"redeploy": "Redeploy",
"no_previews_available": "No previews available"
},
"secrets": {
"secret_saved": "Secret saved.",
"use_isbuildsecret": "Use isBuildSecret",
"secrets_for": "Secrets for"
},
"storage": {
"path_is_required": "Path is required.",
"storage_saved": "Storage saved.",
"storage_updated": "Storage updated.",
"storage_deleted": "Storage deleted.",
"persistent_storage_explainer": "You can specify any folder that you want to be persistent across deployments. <br>This is useful for storing data such as a database (SQLite) or a cache."
},
"deployment_queued": "Deployment queued.",
"confirm_to_delete": "Are you sure you would like to delete '{{name}}'?",
"stop_application": "Stop application",
"permission_denied_stop_application": "You do not have permission to stop the application.",
"rebuild_application": "Rebuild application",
"permission_denied_rebuild_application": "You do not have permission to rebuild application.",
"build_and_start_application": "Build and start application",
"permission_denied_build_and_start_application": "You do not have permission to Build and start application.",
"configurations": "Configurations",
"secret": "Secrets",
"persistent_storage": "Persistent Storage",
"previews": "Previews",
"logs": "Application Logs",
"build_logs": "Build Logs",
"delete_application": "Delete application",
"permission_denied_delete_application": "You do not have permission to delete this application",
"domain_already_in_use": "Domain {{domain}} is already used.",
"dns_not_set_error": "DNS not set or propogated for {{domain}}.<br><br>Please check your DNS settings.",
"settings_saved": "Settings saved.",
"dns_not_set_partial_error": "DNS not set",
"git_source": "Git Source",
"git_repository": "Git Repository",
"build_pack": "Build Pack",
"destination": "Destination",
"application": "Application",
"url_fqdn": "URL (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>",
"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.",
"install_command": "Install Command",
"build_command": "Build 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>.",
"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>.",
"features": "Features",
"enable_automatic_deployment": "Enable Automatic Deployment",
"enable_auto_deploy_webhooks": "Enable automatic deployment through webhooks.",
"enable_mr_pr_previews": "Enable MR/PR Previews",
"enable_preview_deploy_mr_pr_requests": "Enable preview deployments from pull or merge requests.",
"debug_logs": "Debug Logs",
"enable_debug_log_during_build": "Enable debug logs during build phase.<br><span class='text-red-500 font-bold'>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.",
"no_applications_found": "No applications found",
"secret__batch_dot_env": "Paste .env file",
"batch_secrets": "Batch add secrets"
},
"general": "General",
"database": {
"default_database": "Default Database",
"generated_automatically_after_set_to_public": "Generated automatically after set to public",
"connection_string": "Connection String",
"set_public": "Set it public",
"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",
"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>",
"select_database_type": "Select a Database type",
"select_database_version": "Select a Database version",
"confirm_stop": "Are you sure you would like to stop {{name}}?",
"stop_database": "Stop database",
"permission_denied_stop_database": "You do not have permission to stop the database.",
"start_database": "Start database",
"permission_denied_start_database": "You do not have permission to start the database.",
"delete_database": "Delete Database",
"permission_denied_delete_database": "You do not have permission to delete a Database",
"no_databases_found": "No databases found"
},
"destination": {
"delete_destination": "Delete Destination",
"permission_denied_delete_destination": "You do not have permission to delete this destination",
"add_to_coolify": "Add to Coolify",
"coolify_proxy_stopped": "Coolify Proxy stopped!",
"coolify_proxy_started": "Coolify Proxy started!",
"confirm_restart_proxy": "Are you sure you want to restart the proxy? Everything will be reconfigured in ~10 secs.",
"coolify_proxy_restarting": "Coolify Proxy restarting...",
"restarting_please_wait": "Restarting... please wait...",
"force_restart_proxy": "Force restart proxy",
"use_coolify_proxy": "Use Coolify Proxy?",
"no_destination_found": "No destination found",
"new_error_network_already_exists": "Network {{network}} already configured for another team!",
"new": {
"saving_and_configuring_proxy": "Saving and configuring proxy...",
"install_proxy": "This will install a proxy on the destination to allow you to access your applications and services without any manual configuration (recommended for Docker).<br><br>Databases will have their own proxy.",
"add_new_destination": "Add New Destination",
"predefined_destinations": "Predefined destinations"
}
},
"sources": {
"local_docker": "Local Docker",
"remote_docker": "Remote Docker",
"organization_explainer": "Fill it if you would like to use an organization's as your Git Source. Otherwise your user will be used."
},
"source": {
"new": {
"git_source": "Add New Git Source",
"official_providers": "Official providers"
},
"no_git_sources_found": "No git sources found",
"delete_git_source": "Delete Git Source",
"permission_denied": "You do not have permission to delete a Git Source",
"create_new_app": "Create new {{name}} App",
"change_app_settings": "Change {{name}} App Settings",
"install_repositories": "Install Repositories",
"application_id": "Application ID",
"group_name": "Group Name",
"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-orange-600' >in the URL</span> of your GitLab OAuth Application.",
"register_oauth_gitlab": "Register new OAuth application on GitLab",
"gitlab": {
"self_hosted": "Instance-wide application (self-hosted)",
"user_owned": "User owned application",
"group_owned": "Group owned application",
"gitlab_application_type": "GitLab Application Type",
"already_configured": "GitLab App is already configured."
},
"github": {
"redirecting": "Redirecting to Github..."
}
},
"services": {
"all_email_verified": "All email 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."
},
"service": {
"stop_service": "Stop Service",
"permission_denied_stop_service": "You do not have permission to stop the service.",
"start_service": "Start Service",
"permission_denied_start_service": "You do not have permission to start the service.",
"delete_service": "Delete Service",
"permission_denied_delete_service": "You do not have permission to delete a service.",
"no_service": "No services found"
},
"setting": {
"change_language": "Change Language",
"permission_denied": "You do not have permission to do this. \\nAsk an admin to modify your permissions.",
"domain_removed": "Domain removed",
"ssl_explainer": "If you specify <span class='text-yellow-500 font-bold'>https</span>, Coolify will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-yellow-500 font-bold'>www</span>, Coolify will be redirected (302) from non-www and vice versa.",
"must_remove_domain_before_changing": "Must remove the domain before you can change this setting.",
"registration_allowed": "Registration allowed?",
"registration_allowed_explainer": "Allow further registrations to the application. <br>It's turned off after the first registration.",
"coolify_proxy_settings": "Coolify Proxy Settings",
"credential_stat_explainer": "Credentials for <a class=\"text-white font-bold\" href=\"{{link}}\" target=\"_blank\">stats</a> page.",
"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."
},
"team": {
"pending_invitations": "Pending invitations",
"accept": "Accept",
"delete": "Delete",
"member": "member(s)",
"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.",
"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).",
"permission": "Permission",
"you": "(You)",
"promote_to": "Promote to {{grade}}",
"revoke_invitation": "Revoke invitation",
"pending_invitation": "Pending invitation",
"invite_new_member": "Invite new member",
"send_invitation": "Send invitation",
"invite_only_register_explainer": "You can only invite registered users at the moment - will be extended soon.",
"admin": "Admin",
"read": "Read"
}
}

322
src/lib/locales/fr.json Normal file
View File

@@ -0,0 +1,322 @@
{
"application": {
"application": "Application",
"build": {
"build_logs_of": "Créer des journaux de",
"finished_in": "Fini en",
"load_more": "Charger plus",
"no_logs": "Aucun journal trouvé",
"queued": "En file d'attente",
"queued_waiting_exec": "En file d'attente et en attente d'exécution.",
"running": "Fonctionnement",
"waiting_logs": "En attente des logs..."
},
"build_and_start_application": "Build et démarrer l'application",
"build_command": "Commande Build",
"build_logs": "Créer des journaux",
"build_pack": "Pack de Build",
"cant_activate_auto_deploy_without_repo": "Impossible d'activer les déploiements automatiques tant qu'une seule application n'est pas définie pour ce dépôt/branche.",
"configuration": {
"branch_already_in_use": "Cette branche est déjà utilisée par une autre application. \nLes webhooks ne fonctionneront pas dans ce cas pour les deux applications. \nÊtes-vous sûr de vouloir l'utiliser ?",
"buildpack": {
"choose_this_one": "Choisir celui-ci..."
},
"configuration_missing": "Configuration manquante",
"configure_build_pack": "Configurer le pack de build",
"configure_destination": "Configurer la destination",
"configure_it_now": "Configurez-le maintenant",
"found_lock_file": "Fichier .lock trouvé pour <span class=\"font-bold text-orange-500 pl-1\">{{packageManager}}</span>. \nL'utiliser pour les commandes prédéfinies.",
"loading_branches": "Chargement des branches...",
"loading_groups": "Chargement des groupes...",
"loading_projects": "Chargement des projets...",
"loading_repositories": "Chargement des dépôts Git...",
"no_branches_found": "Aucune branche trouvée",
"no_configurable_destination": "Aucune destination configurable trouvée",
"no_configurable_git": "Aucune source Git configurable trouvée",
"no_projects_found": "Aucun projet trouvé",
"no_repositories_configured": "Aucun dépôt Git configuré pour votre application.",
"scanning_repository_suggest_build_pack": "Analyse du dépôt pour vous suggérer un pack de Build...",
"select_a_branch": "Veuillez sélectionner une branche",
"select_a_git_source": "Sélectionnez une source Git",
"select_a_group": "Veuillez sélectionner un groupe",
"select_a_project": "Veuillez sélectionner un projet",
"select_a_repository": "Veuillez sélectionner un dépôt",
"select_a_repository_first": "Veuillez d'abord sélectionner un dépôt",
"select_a_repository_project": "Sélectionnez un dépôt / projet"
},
"configurations": "Configurations",
"confirm_to_delete": "Voulez-vous vraiment supprimer '{{name}}'?",
"debug_logs": "Journaux de débogage",
"delete_application": "Supprimer l'application",
"deployment_queued": "Déploiement en file d'attente.",
"destination": "Destination",
"directory_to_use_explainer": "Répertoire à utiliser comme base pour toutes les commandes.<br>Pourrait être utile avec <span class='text-green-500 font-bold'>monorepos</span>.",
"dns_not_set_error": "DNS non défini ou propagé pour {{domain}}.<br><br>Veuillez vérifier vos paramètres DNS.",
"dns_not_set_partial_error": "DNS non défini",
"domain_already_in_use": "Le domaine {{domain}} est déjà utilisé.",
"domain_fqdn": "Domaine (FQDN)",
"url_fqdn": "URL (FQDN)",
"enable_auto_deploy_webhooks": "Activez le déploiement automatique via des webhooks.",
"enable_automatic_deployment": "Activer le déploiement automatique",
"enable_debug_log_during_build": "Activez les journaux de débogage pendant la phase de build.<br><span class='text-red-500 font-bold'>Les informations sensibles</span> peuvent être visibles et enregistrées dans les journaux.",
"enable_mr_pr_previews": "Activer les aperçus MR/PR",
"enable_preview_deploy_mr_pr_requests": "Activez les déploiements de prévisualisation à partir de demandes d'extraction ou de fusion.",
"features": "Caractéristiques",
"git_repository": "Dépôt Git",
"git_source": "Source Git",
"https_explainer": "Si vous spécifiez <span class='text-green-500 font-bold'>https</span>, l'application sera accessible uniquement via https. \nUn certificat SSL sera généré pour vous.<br>Si vous spécifiez <span class='text-green-500 font-bold'>www</span>, l'application sera redirigée (302) à partir de non-www et vice versa \n.<br><br>Pour modifier le domaine, vous devez d'abord arrêter l'application.<br><br><span class='text-white font-bold'>Vous devez configurer, en avance, votre DNS pour pointer vers l'IP du serveur.</span>",
"install_command": "Commande d'installation",
"logs": "Journaux des applications",
"no_applications_found": "Aucune application trouvée",
"permission_denied_build_and_start_application": "Vous n'êtes pas autorisé à créer et à démarrer l'application.",
"permission_denied_delete_application": "Vous n'êtes pas autorisé à supprimer cette application",
"permission_denied_rebuild_application": "Vous n'êtes pas autorisé à re-build l'application.",
"permission_denied_stop_application": "Vous n'êtes pas autorisé à arrêter l'application.",
"persistent_storage": "Stockage persistant",
"preview": {
"need_during_buildtime": "Besoin pendant la build ?",
"no_previews_available": "Aucun aperçu disponible",
"redeploy": "Redéployer",
"setup_secret_app_first": "Vous pouvez ajouter des secrets aux déploiements PR/MR. \nVeuillez d'abord ajouter des secrets à l'application. \n<br>Utile pour créer des environnements <span class='text-green-500 font-bold'>de mise en scène</span>.",
"values_overwriting_app_secrets": "Ces valeurs remplacent les secrets d'application dans les déploiements PR/MR. \nUtile pour créer des environnements <span class='text-green-500 font-bold'>de mise en scène</span>."
},
"previews": "Aperçus",
"publish_directory_explainer": "Répertoire contenant tous les actifs à déployer. \n<br> Par exemple : <span class='text-green-500 font-bold'>dist</span>,<span class='text-green-500 font-bold'>_site</span> ou <span \nclass='text-green-500 font-bold'>public</span>.",
"rebuild_application": "Re-build l'application",
"secret": "secrets",
"secrets": {
"secret_saved": "Secret enregistré.",
"secrets_for": "secrets pour",
"use_isbuildsecret": "Utiliser isBuildSecret"
},
"settings_saved": "Paramètres sauvegardés.",
"ssl_explainer": "Il générera des certificats pour www et non-www. \n<br>Vous devez avoir <span class='font-bold text-green-500'>les deux entrées DNS</span> définies à l'avance.<br><br>Utile si vous prévoyez d'avoir des visiteurs sur les deux.",
"ssl_www_and_non_www": "Générer SSL pour www et non-www ?",
"start_command": "Démarrer la commande",
"stop_application": "Arrêter l'application",
"storage": {
"path_is_required": "Le chemin est requis.",
"persistent_storage_explainer": "Vous pouvez spécifier n'importe quel dossier que vous souhaitez conserver dans les déploiements. \n<br>Ceci est utile pour stocker des données telles qu'une base de données (SQLite) ou un cache.",
"storage_deleted": "Stockage supprimé.",
"storage_saved": "Stockage enregistré.",
"storage_updated": "Stockage mis à jour."
}
},
"database": {
"change_append_only_mode": "Changer le mode d'ajout uniquement",
"confirm_stop": "Êtes-vous sûr de vouloir arrêter {{name}} ?",
"connection_string": "Connexion string",
"default_database": "Base de données par défaut",
"delete_database": "Supprimer la base de données",
"generated_automatically_after_set_to_public": "Généré automatiquement après avoir été défini sur public",
"no_databases_found": "Aucune base de données trouvée",
"permission_denied_delete_database": "Vous n'êtes pas autorisé à supprimer une base de données",
"permission_denied_start_database": "Vous n'êtes pas autorisé à démarrer la base de données.",
"permission_denied_stop_database": "Vous n'êtes pas autorisé à arrêter la base de données.",
"select_database_type": "Sélectionnez un type de base de données",
"select_database_version": "Sélectionnez une version de la base de données",
"set_public": "Rendre public",
"start_database": "Démarrer la base de données",
"stop_database": "Arrêter la base de données",
"warning_append_only": "Utile si vous souhaitez restaurer des données Redis à partir d'une sauvegarde.<br><span class='font-bold text-white'>Le redémarrage de la base de données est nécessaire.</span>",
"warning_database_public": "Votre base de données sera accessible depuis Internet. \n<br>Prenez la sécurité au sérieux dans ce cas!"
},
"destination": {
"add_to_coolify": "Ajouter à Coolify",
"confirm_restart_proxy": "Voulez-vous vraiment redémarrer le proxy? \nTout sera reconfiguré en ~10 secondes.",
"coolify_proxy_restarting": "Redémarrage du Proxy Coolify...",
"coolify_proxy_started": "Proxy Coolify démarré!",
"coolify_proxy_stopped": "Proxy Coolify arrêté!",
"delete_destination": "Supprimer le destinataire",
"force_restart_proxy": "Forcer le redémarrage du proxy",
"new": {
"add_new_destination": "Ajouter une nouvelle destination",
"install_proxy": "Cela installera un proxy sur la destination pour vous permettre d'accéder à vos applications et services sans aucune configuration manuelle (recommandé pour Docker).<br><br>Les bases de données auront leur propre proxy.",
"predefined_destinations": "Destinations prédéfinies",
"saving_and_configuring_proxy": "Enregistrement et configuration du proxy..."
},
"new_error_network_already_exists": "Réseau {{network}} déjà configuré pour une autre équipe !",
"no_destination_found": "Aucune destination trouvée",
"permission_denied_delete_destination": "Vous n'êtes pas autorisé à supprimer cette destination",
"restarting_please_wait": "Redémarrage... veuillez patienter...",
"use_coolify_proxy": "Utiliser le Proxy Coolify ?"
},
"error": {
"here": "ici",
"you_are_lost": "Oups vous êtes perdu ! \nMais n'ayez pas peur !",
"you_can_find_your_way_back": "Tu peux retrouver ton chemin"
},
"forms": {
"action": "action",
"add": "Ajouter",
"already_used_for": "<span class=\"text-red-500\">{{type}}</span> déjà utilisé pour",
"api_port": "Port API",
"api_url": "URL de l'API",
"base_directory": "Répertoire de base",
"configuration": "Configuration",
"confirm_continue": "Êtes-vous sûr de continuer ?",
"default": "défaut",
"default_email_address": "Adresse e-mail par défaut",
"default_password": "Mot de passe par défaut",
"eg": "ex",
"email": "Adresse e-mail",
"engine": "Moteur",
"extra_config": "Configuration supplémentaire",
"generated_automatically_after_start": "Généré automatiquement après le démarrage",
"host": "Hôte",
"html_url": "URL HTML",
"ip_address": "Adresse IP",
"is_required": "est requis.",
"loading": "Chargement...",
"must_be_stopped_to_modify": "Doit être arrêté pour être modifié.",
"name": "Nom",
"network": "Réseau",
"new_password": "Nouveau mot de passe",
"no_actions_available": "Aucune action disponible",
"organization": "Organisation",
"password": "Mot de passe",
"password_again": "Mot de passe à nouveau",
"passwords_not_match": "Les mots de passe ne correspondent pas.",
"path": "Chemin",
"port": "Port",
"public_port_range": "Gamme de ports publics",
"public_port_range_explainer": "Ports utilisés pour exposer les bases de données/services/services internes.<br> Ajoutez-les à votre pare-feu (le cas échéant).<br><br>Vous pouvez spécifier une plage de ports, par exemple : <span class='text-yellow-500 \nfont-bold'>9000-9100</span>",
"publish_directory": "Publier le répertoire",
"remove": "Retirer",
"remove_domain": "Supprimer le domaine",
"removing": "Suppression...",
"root_db_password": "Mot de passe root de la base de données",
"root_db_user": "Utilisateur root de la base de données",
"root_user": "Utilisateur root",
"roots_password": "Mot de passe de l'utilisateur root",
"save": "sauvegarder",
"saving": "Sauvegarde...",
"select_a_service": "Sélectionnez un service",
"select_a_service_version": "Sélectionnez une version de service",
"set": "Régler",
"ssh_private_key": "Clé privée SSH",
"submit": "Nous faire parvenir",
"super_secure_new_password": "Nouveau mot de passe super sécurisé",
"type": "Taper",
"user": "Utilisateur",
"username": "Nom d'utilisateur",
"value": "Valeur",
"verify_emails_without_smtp": "Vérifier les e-mails sans SMTP",
"verifying": "Vérification",
"version": "Version"
},
"general": "Général",
"index": {
"applications": "Applications",
"dashboard": "Tableau de bord",
"database": "Base de données",
"databases": "Bases de données",
"destinations": "Destinations",
"git_sources": "Sources Git",
"global_settings": "Paramètres globaux",
"logout": "Se déconnecter",
"not_implemented_yet": "Pas encore implémenté",
"secret": "Secret",
"services": "Services",
"settings": "Réglages",
"team": "Équipe",
"teams": "Équipes"
},
"layout": {
"new_version": "Nouvelle version accessible. \nRechargement...",
"switch_to_a_different_team": "Changer d'équipe...",
"update_available": "Mise à jour disponible",
"update_done": "Mise à jour terminée.",
"wait_new_version_startup": "En attendant le lancement de la nouvelle version..."
},
"login": {
"already_logged_in": "Déjà connecté...",
"authenticating": "Authentification...",
"login": "Connexion"
},
"register": {
"first_user": "Vous enregistrez le premier utilisateur. \nCe sera l'administrateur de votre instance Coolify.",
"register": "S'inscrire"
},
"reset": {
"find_path_secret_key": "Vous pouvez le trouver dans ~/coolify/.env (COOLIFY_SECRET_KEY)",
"invalid_secret_key": "Clé secrète invalide.",
"reset_password": "Réinitialiser",
"secret_key": "Clef secrète"
},
"service": {
"delete_service": "Supprimer le service",
"no_service": "Aucun service trouvé",
"permission_denied_delete_service": "Vous n'êtes pas autorisé à supprimer un service.",
"permission_denied_start_service": "Vous n'êtes pas autorisé à démarrer le service.",
"permission_denied_stop_service": "Vous n'êtes pas autorisé à arrêter le service.",
"start_service": "Démarrer le service",
"stop_service": "Stopper le service"
},
"services": {
"all_email_verified": "Tous les e-mails sont vérifiés. \nVous pouvez vous connecter maintenant.",
"generate_www_non_www_ssl": "Il générera des certificats pour www et non-www. \n<br>Vous devez avoir <span class='font-bold text-pink-600'>les deux entrées DNS</span> définies à l'avance.<br><br>Le service devra être redémarré."
},
"setting": {
"coolify_proxy_settings": "Paramètres du proxy Coolify",
"credential_stat_explainer": "Identifiants pour la page <a class=\"text-white font-bold\" href=\"{{link}}\" target=\"_blank\">statistiques</a>.",
"domain_removed": "Domaine supprimé",
"must_remove_domain_before_changing": "Vous devez supprimer le domaine avant de pouvoir modifier ce paramètre.",
"permission_denied": "Vous n'avez pas la permission de faire cela. \n\\nDemandez à un administrateur de modifier vos autorisations.",
"registration_allowed": "Inscription autorisée ?",
"registration_allowed_explainer": "Autoriser d'autres inscriptions à l'application. \n<br>Il est désactivé après la première inscription.",
"ssl_explainer": "Si vous spécifiez <span class='text-yellow-500 font-bold'>https</span>, Coolify sera accessible uniquement via https. \nUn certificat SSL sera généré pour vous.<br>Si vous spécifiez <span class='text-yellow-500 font-bold'>www</span>, Coolify sera redirigé (302) à partir de non-www et vice versa."
},
"source": {
"application_id": "ID d'application",
"change_app_settings": "Modifier les paramètres de l'application {{name}}",
"create_new_app": "Créer une nouvelle application {{name}}",
"delete_git_source": "Supprimer la source Git",
"github": {
"redirecting": "Redirection vers Github..."
},
"gitlab": {
"already_configured": "L'application GitLab est déjà configurée.",
"gitlab_application_type": "Type d'application GitLab",
"group_owned": "Application détenue par le groupe",
"self_hosted": "Application à l'échelle de l'instance (auto-hébergée)",
"user_owned": "Application appartenant à l'utilisateur"
},
"group_name": "Nom de groupe",
"install_repositories": "Installer les dépôts",
"new": {
"git_source": "Ajouter une nouvelle source Git",
"official_providers": "Fournisseurs officiels"
},
"no_git_sources_found": "Aucune source git trouvée",
"oauth_id": "ID OAuth",
"oauth_id_explainer": "L'identifiant OAuth est l'identifiant unique de l'application GitLab. \n<br>Vous pouvez le trouver <span class='font-bold text-orange-600' >dans l'URL</span> de votre application GitLab OAuth.",
"permission_denied": "Vous n'êtes pas autorisé à supprimer une source Git",
"register_oauth_gitlab": "Enregistrer une nouvelle application OAuth sur GitLab"
},
"sources": {
"local_docker": "Docker local",
"organization_explainer": "Remplissez-le si vous souhaitez utiliser une organisation comme source Git. \nSinon, votre utilisateur sera utilisé.",
"remote_docker": "Station d'accueil à distance"
},
"team": {
"accept": "J'accepte",
"admin": "Administrateur",
"delete": "Supprimer",
"invite_new_member": "Inviter un nouveau membre",
"invite_only_register_explainer": "Vous ne pouvez inviter que des utilisateurs enregistrés pour le moment - sera bientôt prolongé.",
"invited_with_permissions": "Invité à <span class=\"font-bold text-pink-600\">{{teamName}}</span> avec <span class=\"font-bold text-rose-600\">{{permission}}</span \n> autorisation.",
"member": "membre(s)",
"members": "Membres",
"pending_invitation": "Invitation en attente",
"pending_invitations": "Invitations en attente",
"permission": "Autorisation",
"promote_to": "Promouvoir à {{grade}}",
"read": "Lire",
"revoke_invitation": "Révoquer l'invitation",
"root": "(suprême)",
"root_team_explainer": "Il s'agit de l'équipe <span class='text-red-500 font-bold'>suprême</span>. \nCela signifie que les membres de ce groupe peuvent gérer les paramètres à l'échelle de l'instance et avoir tous les privilèges dans Coolify (imaginez comme un utilisateur root sous Linux).",
"send_invitation": "Envoyer une invitation",
"you": "(Toi)"
}
}

View File

@@ -0,0 +1,42 @@
import { prisma } from '$lib/database';
import { buildQueue } from '.';
import got from 'got';
import { asyncExecShell, version } from '$lib/common';
import compare from 'compare-versions';
import { dev } from '$app/env';
export default async function (): Promise<void> {
try {
const currentVersion = version;
const { isAutoUpdateEnabled } = await prisma.setting.findFirst();
if (isAutoUpdateEnabled) {
const versions = await got
.get(
`https://get.coollabs.io/versions.json?appId=${process.env['COOLIFY_APP_ID']}&version=${currentVersion}`
)
.json();
const latestVersion = versions['coolify'].main.version;
const isUpdateAvailable = compare(latestVersion, currentVersion);
if (isUpdateAvailable === 1) {
const activeCount = await buildQueue.getActiveCount();
if (activeCount === 0) {
if (!dev) {
await buildQueue.pause();
console.log(`Updating Coolify to ${latestVersion}.`);
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
await asyncExecShell(`env | grep COOLIFY > .env`);
await asyncExecShell(
`docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-redis && docker rm coolify coolify-redis && docker compose up -d --force-recreate"`
);
} else {
await buildQueue.pause();
console.log('Updating (not really in dev mode).');
}
}
}
}
} catch (error) {
await buildQueue.resume();
console.log(error);
}
}

View File

@@ -46,7 +46,8 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
persistentStorage,
pythonWSGI,
pythonModule,
pythonVariable
pythonVariable,
denoOptions
} = job.data;
let {
branch,
@@ -56,7 +57,9 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
buildCommand,
startCommand,
baseDirectory,
publishDirectory
publishDirectory,
dockerFileLocation,
denoMainFile
} = job.data;
const { debug } = settings;
@@ -107,6 +110,8 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
buildCommand = configuration.buildCommand;
publishDirectory = configuration.publishDirectory;
baseDirectory = configuration.baseDirectory;
dockerFileLocation = configuration.dockerFileLocation;
denoMainFile = configuration.denoMainFile;
const commit = await importers[gitSource.type]({
applicationId,
@@ -209,7 +214,10 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
phpModules,
pythonWSGI,
pythonModule,
pythonVariable
pythonVariable,
dockerFileLocation,
denoMainFile,
denoOptions
});
else {
await saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId });
@@ -285,7 +293,18 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
networks: [docker.network],
labels,
depends_on: [],
restart: 'always'
restart: 'always',
// logging: {
// driver: 'fluentd',
// },
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
}
},
networks: {

View File

@@ -2,8 +2,9 @@ import { asyncExecShell, getEngine, version } from '$lib/common';
import { prisma } from '$lib/database';
export default async function (): Promise<void> {
const destinationDockers = await prisma.destinationDocker.findMany();
for (const destinationDocker of destinationDockers) {
const host = getEngine(destinationDocker.engine);
const engines = [...new Set(destinationDockers.map(({ engine }) => engine))];
for (const engine of engines) {
const host = getEngine(engine);
// Cleanup old coolify images
try {
let { stdout: images } = await asyncExecShell(
@@ -14,56 +15,23 @@ export default async function (): Promise<void> {
await asyncExecShell(`DOCKER_HOST=${host} docker rmi -f ${images}`);
}
} catch (error) {
console.log(error);
//console.log(error);
}
try {
await asyncExecShell(`DOCKER_HOST=${host} docker container prune -f`);
} catch (error) {
console.log(error);
//console.log(error);
}
try {
await asyncExecShell(`DOCKER_HOST=${host} docker image prune -f --filter "until=2h"`);
} catch (error) {
console.log(error);
//console.log(error);
}
// Cleanup old images older than a day
try {
await asyncExecShell(`DOCKER_HOST=${host} docker image prune --filter "until=72h" -a -f`);
} catch (error) {
//console.log(error);
}
// Tagging images with labels
// try {
// const images = [
// `coollabsio/${defaultProxyImageTcp}`,
// `coollabsio/${defaultProxyImageHttp}`,
// 'certbot/certbot:latest',
// 'node:16.14.0-alpine',
// 'alpine:latest',
// 'nginx:stable-alpine',
// 'node:lts',
// 'php:apache',
// 'rust:latest'
// ];
// for (const image of images) {
// try {
// await asyncExecShell(`DOCKER_HOST=${host} docker image inspect ${image}`);
// } catch (error) {
// await asyncExecShell(
// `DOCKER_HOST=${host} docker pull ${image} && echo "FROM ${image}" | docker build --label coolify.image="true" -t "${image}" -`
// );
// }
// }
// } catch (error) {}
// if (!dev) {
// // Cleanup images that are not managed by coolify
// try {
// await asyncExecShell(
// `DOCKER_HOST=${host} docker image prune --filter 'label!=coolify.image=true' -a -f`
// );
// } catch (error) {
// console.log(error);
// }
// // Cleanup old images >3 days
// try {
// await asyncExecShell(`DOCKER_HOST=${host} docker image prune --filter "until=72h" -a -f`);
// } catch (error) {
// console.log(error);
// }
// }
}
}

View File

@@ -7,8 +7,10 @@ import builder from './builder';
import logger from './logger';
import cleanup from './cleanup';
import proxy from './proxy';
import proxyTcpHttp from './proxyTcpHttp';
import ssl from './ssl';
import sslrenewal from './sslrenewal';
import autoUpdater from './autoUpdater';
import { asyncExecShell, saveBuildLog } from '$lib/common';
@@ -29,20 +31,26 @@ const connectionOptions = {
const cron = async (): Promise<void> => {
new QueueScheduler('proxy', connectionOptions);
new QueueScheduler('proxyTcpHttp', connectionOptions);
new QueueScheduler('cleanup', connectionOptions);
new QueueScheduler('ssl', connectionOptions);
new QueueScheduler('sslRenew', connectionOptions);
new QueueScheduler('autoUpdater', connectionOptions);
const queue = {
proxy: new Queue('proxy', { ...connectionOptions }),
proxyTcpHttp: new Queue('proxyTcpHttp', { ...connectionOptions }),
cleanup: new Queue('cleanup', { ...connectionOptions }),
ssl: new Queue('ssl', { ...connectionOptions }),
sslRenew: new Queue('sslRenew', { ...connectionOptions })
sslRenew: new Queue('sslRenew', { ...connectionOptions }),
autoUpdater: new Queue('autoUpdater', { ...connectionOptions })
};
await queue.proxy.drain();
await queue.proxyTcpHttp.drain();
await queue.cleanup.drain();
await queue.ssl.drain();
await queue.sslRenew.drain();
await queue.autoUpdater.drain();
new Worker(
'proxy',
@@ -54,6 +62,16 @@ const cron = async (): Promise<void> => {
}
);
new Worker(
'proxyTcpHttp',
async () => {
await proxyTcpHttp();
},
{
...connectionOptions
}
);
new Worker(
'ssl',
async () => {
@@ -84,10 +102,22 @@ const cron = async (): Promise<void> => {
}
);
new Worker(
'autoUpdater',
async () => {
await autoUpdater();
},
{
...connectionOptions
}
);
await queue.proxy.add('proxy', {}, { repeat: { every: 10000 } });
await queue.proxyTcpHttp.add('proxyTcpHttp', {}, { repeat: { every: 10000 } });
await queue.ssl.add('ssl', {}, { repeat: { every: dev ? 10000 : 60000 } });
if (!dev) await queue.cleanup.add('cleanup', {}, { repeat: { every: 300000 } });
await queue.sslRenew.add('sslRenew', {}, { repeat: { every: 1800000 } });
await queue.autoUpdater.add('autoUpdater', {}, { repeat: { every: 60000 } });
};
cron().catch((error) => {
console.log('cron failed to start');
@@ -100,6 +130,9 @@ const buildWorker = new Worker(buildQueueName, async (job) => await builder(job)
concurrency: 1,
...connectionOptions
});
buildQueue.resume().catch((err) => {
console.log('Build queue failed to resume!', err);
});
buildWorker.on('completed', async (job: Bullmq.Job) => {
try {
@@ -108,7 +141,6 @@ buildWorker.on('completed', async (job: Bullmq.Job) => {
setTimeout(async () => {
await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'success' } });
}, 1234);
console.log(error);
} finally {
const workdir = `/tmp/build-sources/${job.data.repository}/${job.data.build_id}`;
if (!dev) await asyncExecShell(`rm -fr ${workdir}`);
@@ -124,7 +156,6 @@ buildWorker.on('failed', async (job: Bullmq.Job, failedReason) => {
setTimeout(async () => {
await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'failed' } });
}, 1234);
console.log(error);
} finally {
const workdir = `/tmp/build-sources/${job.data.repository}`;
if (!dev) await asyncExecShell(`rm -fr ${workdir}`);

View File

@@ -0,0 +1,55 @@
import { ErrorHandler, generateDatabaseConfiguration, prisma } from '$lib/database';
import { startCoolifyProxy, startHttpProxy, startTcpProxy } from '$lib/haproxy';
export default async function (): Promise<void | {
status: number;
body: { message: string; error: string };
}> {
try {
// Coolify Proxy
const localDocker = await prisma.destinationDocker.findFirst({
where: { engine: '/var/run/docker.sock' }
});
if (localDocker && localDocker.isCoolifyProxyUsed) {
await startCoolifyProxy('/var/run/docker.sock');
}
// TCP Proxies
const databasesWithPublicPort = await prisma.database.findMany({
where: { publicPort: { not: null } },
include: { settings: true, destinationDocker: true }
});
for (const database of databasesWithPublicPort) {
const { destinationDockerId, destinationDocker, publicPort, id } = database;
if (destinationDockerId) {
const { privatePort } = generateDatabaseConfiguration(database);
await startTcpProxy(destinationDocker, id, publicPort, privatePort);
}
}
const wordpressWithFtp = await prisma.wordpress.findMany({
where: { ftpPublicPort: { not: null } },
include: { service: { include: { destinationDocker: true } } }
});
for (const ftp of wordpressWithFtp) {
const { service, ftpPublicPort } = ftp;
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId) {
await startTcpProxy(destinationDocker, `${id}-ftp`, ftpPublicPort, 22);
}
}
// HTTP Proxies
const minioInstances = await prisma.minio.findMany({
where: { publicPort: { not: null } },
include: { service: { include: { destinationDocker: true } } }
});
for (const minio of minioInstances) {
const { service, publicPort } = minio;
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId) {
await startHttpProxy(destinationDocker, id, publicPort, 9000);
}
}
} catch (error) {
return ErrorHandler(error.response?.body || error);
}
}

View File

@@ -1,8 +1,6 @@
export const publicPaths = [
'/login',
'/register',
'/reset',
'/reset/password',
'/webhooks/success',
'/webhooks/github',
'/webhooks/github/install',

View File

@@ -5,3 +5,4 @@ export const gitTokens: Writable<{ githubToken: string | null; gitlabToken: stri
githubToken: null,
gitlabToken: null
});
export const disabledButton: Writable<boolean> = writable(false);

25
src/lib/translations.ts Normal file
View File

@@ -0,0 +1,25 @@
import i18n from 'sveltekit-i18n';
import lang from './lang.json';
/** @type {import('sveltekit-i18n').Config} */
export const config = {
fallbackLocale: 'en',
translations: {
en: { lang },
fr: { lang }
},
loaders: [
{
locale: 'en',
key: '',
loader: async () => (await import('./locales/en.json')).default
},
{
locale: 'fr',
key: '',
loader: async () => (await import('./locales/fr.json')).default
}
]
};
export const { t, locales, locale, loadTranslations } = new i18n(config);

View File

@@ -21,6 +21,9 @@ export type BuilderJob = {
pythonWSGI: string;
pythonModule: string;
pythonVariable: string;
dockerFileLocation: string;
denoMainFile: string;
denoOptions: string;
createdAt: string;
updatedAt: string;
destinationDockerId: string;

View File

@@ -23,6 +23,14 @@ export type ComposeFileService = {
dockerfile: string;
args?: Record<string, unknown>;
};
deploy?: {
restart_policy?: {
condition?: string;
delay?: string;
max_attempts?: number;
window?: string;
};
};
};
export type ComposerFileVersion =

View File

@@ -12,15 +12,18 @@
</script>
<script>
import { t } from '$lib/translations';
export let status;
export let error;
</script>
<div class="mx-auto flex h-screen flex-col items-center justify-center px-4">
<div class="pb-10 text-7xl font-bold">{status}</div>
<div class="text-3xl font-bold">Ooops you are lost! But don't be afraid!</div>
<div class="text-3xl font-bold">{$t('error.you_are_lost')}</div>
<div class="text-xl">
You can find your way back <a href="/" class="font-bold uppercase text-sky-400">here</a>
{$t('error.you_can_find_your_way_back')}
<a href="/" class="font-bold uppercase text-sky-400">{$t('error.here')}</a>
</div>
<div class="py-10 text-xs font-bold">
<pre

View File

@@ -1,8 +1,15 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
import { publicPaths } from '$lib/settings';
import { locale, loadTranslations } from '$lib/translations';
export const load: Load = async ({ fetch, url, session }) => {
const { pathname } = url;
const defaultLocale = 'en';
const sessionLocale = session.lang;
const initLocale = sessionLocale || locale.get() || defaultLocale;
await loadTranslations(initLocale, pathname);
if (!session.userId && !publicPaths.includes(url.pathname)) {
return {
status: 302,
@@ -39,7 +46,6 @@
import { asyncSleep } from '$lib/components/common';
import { del, get, post } from '$lib/api';
import { browser, dev } from '$app/env';
let isUpdateAvailable = false;
let updateStatus = {
@@ -456,7 +462,6 @@
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85" />
</svg>
</a>
{#if $session.teamId === '0'}
<a
sveltekit:prefetch

View File

@@ -17,7 +17,7 @@
const endpoint = `/applications/${params.id}.json`;
const res = await fetch(endpoint);
if (res.ok) {
let { application, isRunning, appId, githubToken, gitlabToken } = await res.json();
let { application, isRunning, isExited, appId, githubToken, gitlabToken } = await res.json();
if (!application || Object.entries(application).length === 0) {
return {
status: 302,
@@ -46,6 +46,7 @@
props: {
application,
isRunning,
isExited,
githubToken,
gitlabToken
},
@@ -67,27 +68,39 @@
<script lang="ts">
export let application;
export let isRunning;
export let isExited;
export let githubToken;
export let gitlabToken;
import { page, session } from '$app/stores';
import { errorNotification } from '$lib/form';
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import Loading from '$lib/components/Loading.svelte';
import { del, post } from '$lib/api';
import { del, get, post } from '$lib/api';
import { goto } from '$app/navigation';
import { gitTokens } from '$lib/store';
import { toast } from '@zerodevx/svelte-toast';
import { disabledButton } from '$lib/store';
import { onDestroy, onMount } from 'svelte';
import { t } from '$lib/translations';
if (githubToken) $gitTokens.githubToken = githubToken;
if (gitlabToken) $gitTokens.gitlabToken = gitlabToken;
let loading = false;
let statusInterval;
$disabledButton =
!$session.isAdmin ||
!application.fqdn ||
!application.gitSource ||
!application.repository ||
!application.destinationDocker ||
!application.buildPack;
const { id } = $page.params;
async function handleDeploySubmit() {
try {
const { buildId } = await post(`/applications/${id}/deploy.json`, { ...application });
toast.push('Deployment queued.');
toast.push($t('application.deployment_queued'));
if ($page.url.pathname.startsWith(`/applications/${id}/logs/build`)) {
return window.location.assign(`/applications/${id}/logs/build?buildId=${buildId}`);
} else {
@@ -101,7 +114,7 @@
}
async function deleteApplication(name) {
const sure = confirm(`Are you sure you would like to delete '${name}'?`);
const sure = confirm($t('application.confirm_to_delete', { name }));
if (sure) {
loading = true;
try {
@@ -121,142 +134,86 @@
return errorNotification(error);
}
}
async function getStatus() {
statusInterval = setInterval(async () => {
const data = await get(`/applications/${id}.json`);
isRunning = data.isRunning;
isExited = data.isExited;
}, 1000);
}
onDestroy(() => {
clearInterval(statusInterval);
});
onMount(async () => {
await getStatus();
});
</script>
<nav class="nav-side">
{#if loading}
<Loading fullscreen cover />
{:else}
{#if application.fqdn && application.gitSource && application.repository && application.destinationDocker && application.buildPack}
{#if isRunning}
{#if isExited}
<a
href={!$disabledButton ? `/applications/${id}/logs` : null}
class=" icons bg-transparent tooltip-bottom text-sm flex items-center text-red-500 tooltip-red-500"
data-tooltip="Application exited with an error!"
sveltekit:prefetch
>
<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="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"
/>
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
</a>
{/if}
{#if isRunning}
<button
on:click={stopApplication}
title="Stop application"
type="submit"
disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-red-500"
data-tooltip={$session.isAdmin
? $t('application.stop_application')
: $t('application.permission_denied_stop_application')}
>
<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>
<form on:submit|preventDefault={handleDeploySubmit}>
<button
on:click={stopApplication}
title="Stop application"
title="Rebuild application"
type="submit"
disabled={!$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-red-500"
disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 hover:text-green-500"
data-tooltip={$session.isAdmin
? 'Stop application'
: 'You do not have permission to stop the application.'}
>
<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>
<form on:submit|preventDefault={handleDeploySubmit}>
<button
title="Rebuild application"
type="submit"
disabled={!$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 hover:text-green-500"
data-tooltip={$session.isAdmin
? 'Rebuild application'
: 'You do not have permission to rebuild application.'}
>
<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="M16.3 5h.7a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h5l-2.82 -2.82m0 5.64l2.82 -2.82"
transform="rotate(-45 12 12)"
/>
</svg>
</button>
</form>
{:else}
<form on:submit|preventDefault={handleDeploySubmit}>
<button
title="Build and start application"
type="submit"
disabled={!$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-green-500"
data-tooltip={$session.isAdmin
? 'Build and start application'
: 'You do not have permission to Build and start application.'}
>
<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 4v16l13 -8z" />
</svg>
</button>
</form>
{/if}
<div class="border border-stone-700 h-8" />
<a
href="/applications/{id}"
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
title="Configurations"
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
data-tooltip="Configurations"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="4" y="8" width="4" height="4" />
<line x1="6" y1="4" x2="6" y2="8" />
<line x1="6" y1="12" x2="6" y2="20" />
<rect x="10" y="14" width="4" height="4" />
<line x1="12" y1="4" x2="12" y2="14" />
<line x1="12" y1="18" x2="12" y2="20" />
<rect x="16" y="5" width="4" height="4" />
<line x1="18" y1="4" x2="18" y2="5" />
<line x1="18" y1="9" x2="18" y2="20" />
</svg></button
></a
>
<a
href="/applications/{id}/secrets"
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
title="Secret"
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
data-tooltip="Secret"
? 'Rebuild application'
: 'You do not have permission to rebuild application.'}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -270,24 +227,22 @@
>
<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"
d="M16.3 5h.7a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h5l-2.82 -2.82m0 5.64l2.82 -2.82"
transform="rotate(-45 12 12)"
/>
<circle cx="12" cy="11" r="1" />
<line x1="12" y1="12" x2="12" y2="14.5" />
</svg></button
></a
>
<a
href="/applications/{id}/storage"
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/applications/${id}/storage`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/storage`}
>
</svg>
</button>
</form>
{:else}
<form on:submit|preventDefault={handleDeploySubmit}>
<button
title="Persistent Storage"
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
data-tooltip="Persistent Storage"
title="Build and start application"
type="submit"
disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-green-500"
data-tooltip={$session.isAdmin
? 'Build and start application'
: 'You do not have permission to Build and start application.'}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -300,122 +255,223 @@
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" />
<path d="M7 4v16l13 -8z" />
</svg>
</button></a
>
<a
href="/applications/{id}/previews"
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
title="Previews"
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
data-tooltip="Previews"
>
<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></button
></a
>
<div class="border border-stone-700 h-8" />
<a
href="/applications/{id}/logs"
sveltekit:prefetch
class="hover:text-sky-500 rounded"
class:text-sky-500={$page.url.pathname === `/applications/${id}/logs`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs`}
>
<button
title="Application Logs"
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500 "
data-tooltip="Application Logs"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 19a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<path d="M3 6a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<line x1="3" y1="6" x2="3" y2="19" />
<line x1="12" y1="6" x2="12" y2="19" />
<line x1="21" y1="6" x2="21" y2="19" />
</svg>
</button></a
>
<a
href="/applications/{id}/logs/build"
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
title="Build Logs"
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500 "
data-tooltip="Build Logs"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<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
>
<div class="border border-stone-700 h-8" />
</button>
</form>
{/if}
<div class="border border-coolgray-500 h-8" />
<a
href={!$disabledButton ? `/applications/${id}` : null}
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
title="Configurations"
disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm"
data-tooltip="Configurations"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="4" y="8" width="4" height="4" />
<line x1="6" y1="4" x2="6" y2="8" />
<line x1="6" y1="12" x2="6" y2="20" />
<rect x="10" y="14" width="4" height="4" />
<line x1="12" y1="4" x2="12" y2="14" />
<line x1="12" y1="18" x2="12" y2="20" />
<rect x="16" y="5" width="4" height="4" />
<line x1="18" y1="4" x2="18" y2="5" />
<line x1="18" y1="9" x2="18" y2="20" />
</svg></button
></a
>
<a
href={!$disabledButton ? `/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
title="Secret"
disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm"
data-tooltip="Secret"
>
<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
>
<a
href={!$disabledButton ? `/applications/${id}/storage` : null}
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/applications/${id}/storage`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/storage`}
>
<button
title="Persistent Storage"
disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm"
data-tooltip="Persistent Storage"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<ellipse cx="12" cy="6" rx="8" ry="3" />
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
</svg>
</button></a
>
<a
href={!$disabledButton ? `/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
title="Previews"
disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm"
data-tooltip="Previews"
>
<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></button
></a
>
<div class="border border-coolgray-500 h-8" />
<a
href={!$disabledButton ? `/applications/${id}/logs` : null}
sveltekit:prefetch
class="hover:text-sky-500 rounded"
class:text-sky-500={$page.url.pathname === `/applications/${id}/logs`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs`}
>
<button
title={$t('application.logs')}
disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm"
data-tooltip={$t('application.logs')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 19a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<path d="M3 6a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<line x1="3" y1="6" x2="3" y2="19" />
<line x1="12" y1="6" x2="12" y2="19" />
<line x1="21" y1="6" x2="21" y2="19" />
</svg>
</button></a
>
<a
href={!$disabledButton ? `/applications/${id}/logs/build` : null}
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
title="Build Logs"
disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm"
data-tooltip="Build Logs"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<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
>
<div class="border border-coolgray-500 h-8" />
<button
on:click={() => deleteApplication(application.name)}
title="Delete application"
title={$t('application.delete_application')}
type="submit"
disabled={!$session.isAdmin}
class:hover:text-red-500={$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm"
data-tooltip={$session.isAdmin
? 'Delete application'
: 'You do not have permission to delete this application'}
? $t('application.delete_application')
: $t('application.permission_denied_delete_application')}
>
<DeleteIcon />
</button>

View File

@@ -4,6 +4,7 @@ import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
import { promises as dns } from 'dns';
import { t } from '$lib/translations';
export const post: RequestHandler = async (event) => {
const { status, body } = await getUserDetails(event);
@@ -18,7 +19,9 @@ export const post: RequestHandler = async (event) => {
const found = await db.isDomainConfigured({ id, fqdn });
if (found) {
throw {
message: `Domain ${getDomain(fqdn).replace('www.', '')} is already used.`
message: t.get('application.domain_already_in_use', {
domain: getDomain(fqdn).replace('www.', '')
})
};
}
if (!dev && !forceSave) {
@@ -36,7 +39,7 @@ export const post: RequestHandler = async (event) => {
if (localIp?.length > 0) {
if (ip?.length === 0 || !ip.includes(localIp[0])) {
throw {
message: `DNS not set or propogated for ${domain}.<br><br>Please check your DNS settings.`
message: t.get('application.dns_not_set_error', { domain: domain })
};
}
}

View File

@@ -5,6 +5,7 @@
import { post } from '$lib/api';
import { findBuildPack } from '$lib/components/templates';
import { errorNotification } from '$lib/form';
import { t } from '$lib/translations';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
@@ -19,13 +20,14 @@
const tempBuildPack = JSON.parse(
JSON.stringify(findBuildPack(buildPack.name, packageManager))
);
delete tempBuildPack.name;
delete tempBuildPack.fancyName;
delete tempBuildPack.color;
delete tempBuildPack.hoverColor;
if (foundConfig.buildPack !== name) {
await post(`/applications/${id}.json`, { ...tempBuildPack });
await post(`/applications/${id}.json`, { ...tempBuildPack, buildPack: name });
}
await post(`/applications/${id}/configuration/buildpack.json`, { buildPack: name });
return await goto(from || `/applications/${id}`);
@@ -42,7 +44,9 @@
buildPack.name && buildPack.color}"
><span>{buildPack.fancyName}</span>
{#if !scanning && foundConfig?.name === buildPack.name}
<span class="absolute bottom-0 pb-2 text-xs">Choose this one...</span>
<span class="absolute bottom-0 pb-2 text-xs"
>{$t('application.configuration.buildpack.choose_this_one')}</span
>
{/if}
</button>
</form>

View File

@@ -7,6 +7,7 @@
import { errorNotification } from '$lib/form';
import { onMount } from 'svelte';
import { gitTokens } from '$lib/store';
import { t } from '$lib/translations';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
@@ -36,8 +37,15 @@
});
}
async function loadBranchesByPage(page = 0) {
return await get(`${apiUrl}/repos/${selected.repository}/branches?per_page=100&page=${page}`, {
Authorization: `token ${$gitTokens.githubToken}`
});
}
let reposSelectOptions;
let branchSelectOptions;
async function loadRepositories() {
let page = 1;
let reposCount = 0;
@@ -58,24 +66,28 @@
}));
}
async function loadBranches(event) {
branches = [];
selected.repository = event.detail.value;
loading.branches = true;
selected.branch = undefined;
selected.projectId = repositories.find((repo) => repo.full_name === selected.repository).id;
try {
branches = await get(`${apiUrl}/repos/${selected.repository}/branches`, {
Authorization: `token ${$gitTokens.githubToken}`
});
branchSelectOptions = branches.map((branch) => ({
value: branch.name,
label: branch.name
}));
return;
} catch ({ error }) {
return errorNotification(error);
} finally {
loading.branches = false;
let page = 1;
let branchCount = 0;
loading.branches = true;
const loadedBranches = await loadBranchesByPage();
branches = branches.concat(loadedBranches);
branchCount = branches.length;
if (branchCount === 100) {
while (branchCount === 100) {
page = page + 1;
const nextBranches = await loadBranchesByPage(page);
branches = branches.concat(nextBranches);
branchCount = nextBranches.length;
}
}
loading.branches = false;
branchSelectOptions = branches.map((branch) => ({
value: branch.name,
label: branch.name
}));
}
async function isBranchAlreadyUsed(event) {
selected.branch = event.detail.value;
@@ -84,9 +96,7 @@
`/applications/${id}/configuration/repository.json?repository=${selected.repository}&branch=${selected.branch}`
);
if (data.used) {
const sure = confirm(
`This branch is already used by another application. Webhooks won't work in this case for both applications. Are you sure you want to use it?`
);
const sure = confirm($t('application.configuration.branch_already_in_use'));
if (sure) {
selected.autodeploy = false;
showSave = true;
@@ -160,36 +170,44 @@
{#if repositories.length === 0 && loading.repositories === false}
<div class="flex-col text-center">
<div class="pb-4">No repositories configured for your Git Application.</div>
<a href={`/sources/${application.gitSource.id}`}><button>Configure it now</button></a>
<div class="pb-4">{$t('application.configuration.no_repositories_configured')}</div>
<a href={`/sources/${application.gitSource.id}`}
><button>{$t('application.configuration.configure_it_now')}</button></a
>
</div>
{:else}
<form on:submit|preventDefault={handleSubmit} class="flex flex-col justify-center text-center">
<div class="flex-col space-y-3 md:space-y-0 space-x-1">
<div class="flex gap-4">
<div class="flex-col md:flex gap-4">
<div class="custom-select-wrapper">
<Select
placeholder={loading.repositories
? 'Loading repositories ...'
: 'Please select a repository'}
? $t('application.configuration.loading_repositories')
: $t('application.configuration.select_a_repository')}
id="repository"
showIndicator={true}
isWaiting={loading.repositories}
on:select={loadBranches}
items={reposSelectOptions}
isDisabled={loading.repositories}
isClearable={false}
/>
</div>
<input class="hidden" bind:value={selected.projectId} name="projectId" />
<div class="custom-select-wrapper">
<Select
placeholder={loading.branches
? 'Loading branches ...'
? $t('application.configuration.loading_branches')
: !selected.repository
? 'Please select a repository first'
: 'Please select a branch'}
id="repository"
? $t('application.configuration.select_a_repository_first')
: $t('application.configuration.select_a_branch')}
isWaiting={loading.branches}
showIndicator={selected.repository}
id="branches"
on:select={isBranchAlreadyUsed}
items={branchSelectOptions}
isDisabled={loading.branches || !selected.repository}
isClearable={false}
/>
</div>
</div>
@@ -200,15 +218,8 @@
type="submit"
disabled={!showSave}
class:bg-orange-600={showSave}
class:hover:bg-orange-500={showSave}>Save</button
class:hover:bg-orange-500={showSave}>{$t('forms.save')}</button
>
<!-- <button class="w-40"
><a
class="no-underline"
href="{apiUrl}/apps/{application.gitSource.githubApp.name}/installations/new"
>Modify Repositories</a
></button
> -->
</div>
</form>
{/if}

View File

@@ -9,6 +9,7 @@
import { goto } from '$app/navigation';
import { del, get, post, put } from '$lib/api';
import { gitTokens } from '$lib/store';
import { t } from '$lib/translations';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
@@ -139,9 +140,7 @@
`/applications/${id}/configuration/repository.json?repository=${selected.project.path_with_namespace}&branch=${selected.branch.name}`
);
if (data.used) {
const sure = confirm(
`This branch is already used by another application. Webhooks won't work in this case for both applications. Are you sure you want to use it?`
);
const sure = confirm($t('application.configuration.branch_already_in_use'));
if (sure) {
autodeploy = false;
showSave = true;
@@ -270,11 +269,11 @@
<div class="flex flex-col space-y-2 px-4 xl:flex-row xl:space-y-0 xl:space-x-2 ">
{#if loading.base}
<select name="group" disabled class="w-96">
<option selected value="">Loading groups...</option>
<option selected value="">{$t('application.configuration.loading_groups')}</option>
</select>
{:else}
<select name="group" class="w-96" bind:value={selected.group} on:change={loadProjects}>
<option value="" disabled selected>Please select a group</option>
<option value="" disabled selected>{$t('application.configuration.select_a_group')}</option>
{#each groups as group}
<option value={group}>{group.full_name}</option>
{/each}
@@ -282,7 +281,7 @@
{/if}
{#if loading.projects}
<select name="project" disabled class="w-96">
<option selected value="">Loading projects...</option>
<option selected value="">{$t('application.configuration.loading_projects')}</option>
</select>
{:else if !loading.projects && projects.length > 0}
<select
@@ -292,20 +291,24 @@
on:change={loadBranches}
disabled={!selected.group}
>
<option value="" disabled selected>Please select a project</option>
<option value="" disabled selected
>{$t('application.configuration.select_a_project')}</option
>
{#each projects as project}
<option value={project}>{project.name}</option>
{/each}
</select>
{:else}
<select name="project" disabled class="w-96">
<option disabled selected value="">No projects found</option>
<option disabled selected value=""
>{$t('application.configuration.no_projects_found')}</option
>
</select>
{/if}
{#if loading.branches}
<select name="branch" disabled class="w-96">
<option selected value="">Loading branches...</option>
<option selected value="">{$t('application.configuration.loading_branches')}</option>
</select>
{:else if !loading.branches && branches.length > 0}
<select
@@ -315,14 +318,17 @@
on:change={isBranchAlreadyUsed}
disabled={!selected.project}
>
<option value="" disabled selected>Please select a branch</option>
<option value="" disabled selected>{$t('application.configuration.select_a_branch')}</option
>
{#each branches as branch}
<option value={branch}>{branch.name}</option>
{/each}
</select>
{:else}
<select name="project" disabled class="w-96">
<option disabled selected value="">No branches found</option>
<option disabled selected value=""
>{$t('application.configuration.no_branches_found')}</option
>
</select>
{/if}
</div>
@@ -334,7 +340,7 @@
disabled={!showSave || loading.save}
class:bg-orange-600={showSave && !loading.save}
class:hover:bg-orange-500={showSave && !loading.save}
>{loading.save ? 'Saving...' : 'Save'}</button
>{loading.save ? $t('forms.saving') : $t('forms.save')}</button
>
</div>
</form>

View File

@@ -31,11 +31,12 @@
import { buildPacks, findBuildPack, scanningTemplates } from '$lib/components/templates';
import BuildPack from './_BuildPack.svelte';
import { page, session } from '$app/stores';
import { page } from '$app/stores';
import { get } from '$lib/api';
import { errorNotification } from '$lib/form';
import { gitTokens } from '$lib/store';
import { browser } from '$app/env';
import { t } from '$lib/translations';
const { id } = $page.params;
@@ -204,24 +205,27 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Configure Build Pack</div>
<div class="mr-4 text-2xl tracking-tight">
{$t('application.configuration.configure_build_pack')}
</div>
</div>
{#if scanning}
<div class="flex justify-center space-x-1 p-6 font-bold">
<div class="text-xl tracking-tight">Scanning repository to suggest a build pack for you...</div>
<div class="text-xl tracking-tight">
{$t('application.configuration.scanning_repository_suggest_build_pack')}
</div>
</div>
{:else}
{#if packageManager === 'yarn' || packageManager === 'pnpm'}
<div class="flex justify-center p-6">
Found lock file for <span class="font-bold text-orange-500 pl-1">{packageManager}</span>.
Using it for predefined commands commands.
{@html $t('application.configuration.found_lock_file', { packageManager })}
</div>
{/if}
<div class="max-w-7xl mx-auto flex flex-wrap justify-center">
{#each buildPacks as buildPack}
<div class="p-2">
<BuildPack {buildPack} {scanning} {packageManager} bind:foundConfig />
<BuildPack {buildPack} {scanning} bind:foundConfig />
</div>
{/each}
</div>

View File

@@ -33,6 +33,7 @@
import { errorNotification } from '$lib/form';
import { goto } from '$app/navigation';
import { post } from '$lib/api';
import { t } from '$lib/translations';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
@@ -60,12 +61,14 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Configure Destination</div>
<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}
<div class="flex-col">
<div class="pb-2">No configurable Destination found</div>
<div class="pb-2">{$t('application.configuration.no_configurable_destination')}</div>
<div class="flex justify-center">
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
<svg

View File

@@ -18,6 +18,8 @@
</script>
<script lang="ts">
import { t } from '$lib/translations';
export let application;
export let appId;
@@ -26,7 +28,9 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Select a Repository / Project</div>
<div class="mr-4 text-2xl tracking-tight">
{$t('application.configuration.select_a_repository_project')}
</div>
</div>
<div class="flex flex-wrap justify-center">
{#if application.gitSource.type === 'github'}

View File

@@ -33,6 +33,7 @@
import { errorNotification } from '$lib/form';
import { goto } from '$app/navigation';
import { post } from '$lib/api';
import { t } from '$lib/translations';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
@@ -72,12 +73,14 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Select a Git Source</div>
<div class="mr-4 text-2xl tracking-tight">
{$t('application.configuration.select_a_git_source')}
</div>
</div>
<div class="flex flex-col justify-center">
{#if !filteredSources || ownSources.length === 0}
<div class="flex-col">
<div class="pb-2 text-center">No configurable Git Source found</div>
<div class="pb-2 text-center">{$t('application.configuration.no_configurable_git')}</div>
<div class="flex justify-center">
<button on:click={newSource} class="add-icon bg-orange-600 hover:bg-orange-500">
<svg
@@ -139,7 +142,7 @@
<div class="font-bold text-xl text-center truncate">{source.name}</div>
{#if source.gitlabApp && !source.gitlabAppId}
<div class="font-bold text-center truncate text-red-500 group-hover:text-white">
Configuration missing
{$t('application.configuration.configuration_missing')}
</div>
{/if}
</button>

View File

@@ -35,6 +35,7 @@ export const post: RequestHandler = async (event) => {
data: {
id: buildId,
applicationId: id,
branch: applicationFound.branch,
destinationDockerId: applicationFound.destinationDocker.id,
gitSourceId: applicationFound.gitSource.id,
githubAppId: applicationFound.gitSource.githubApp?.id,

View File

@@ -1,7 +1,7 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import { checkContainer } from '$lib/haproxy';
import { checkContainer, isContainerExited } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit';
import jsonwebtoken from 'jsonwebtoken';
import { get as getRequest } from '$lib/api';
@@ -15,17 +15,20 @@ export const get: RequestHandler = async (event) => {
const appId = process.env['COOLIFY_APP_ID'];
let isRunning = false;
let isExited = false;
let githubToken = event.locals.cookies?.githubToken || null;
let gitlabToken = event.locals.cookies?.gitlabToken || null;
try {
const application = await db.getApplication({ id, teamId });
if (application.destinationDockerId) {
isRunning = await checkContainer(application.destinationDocker.engine, id);
isExited = await isContainerExited(application.destinationDocker.engine, id);
}
return {
status: 200,
body: {
isRunning,
isExited,
application,
appId,
githubToken,
@@ -56,9 +59,13 @@ export const post: RequestHandler = async (event) => {
publishDirectory,
pythonWSGI,
pythonModule,
pythonVariable
pythonVariable,
dockerFileLocation,
denoMainFile,
denoOptions
} = await event.request.json();
if (port) port = Number(port);
if (denoOptions) denoOptions = denoOptions.trim();
try {
const defaultConfiguration = await setDefaultConfiguration({
@@ -68,7 +75,9 @@ export const post: RequestHandler = async (event) => {
startCommand,
buildCommand,
publishDirectory,
baseDirectory
baseDirectory,
dockerFileLocation,
denoMainFile
});
await db.configureApplication({
id,
@@ -84,6 +93,9 @@ export const post: RequestHandler = async (event) => {
pythonWSGI,
pythonModule,
pythonVariable,
dockerFileLocation,
denoMainFile,
denoOptions,
...defaultConfiguration
});
return { status: 201 };

View File

@@ -48,6 +48,8 @@
import { post } from '$lib/api';
import cuid from 'cuid';
import { browser } from '$app/env';
import { disabledButton } from '$lib/store';
import { t } from '$lib/translations';
const { id } = $page.params;
let domainEl: HTMLInputElement;
@@ -68,11 +70,6 @@
value: 'Gunicorn',
label: 'Gunicorn'
}
// },
// {
// value: 'uWSGI',
// label: 'uWSGI'
// }
];
if (browser && window.location.hostname === 'demo.coolify.io' && !application.fqdn) {
@@ -105,7 +102,7 @@
branch: application.branch,
projectId: application.projectId
});
return toast.push('Settings saved.');
return toast.push($t('application.settings_saved'));
} catch ({ error }) {
if (name === 'debug') {
debug = !debug;
@@ -127,9 +124,10 @@
try {
await post(`/applications/${id}/check.json`, { fqdn: application.fqdn, forceSave });
await post(`/applications/${id}.json`, { ...application });
return window.location.reload();
$disabledButton = false;
return toast.push('Configurations saved.');
} catch ({ error }) {
if (error?.startsWith('DNS not set')) {
if (error?.startsWith($t('application.dns_not_set_partial_error'))) {
forceSave = true;
}
return errorNotification(error);
@@ -219,7 +217,7 @@
<!-- svelte-ignore missing-declaration -->
<form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex space-x-1 pb-5 font-bold">
<div class="title">General</div>
<div class="title">{$t('general')}</div>
{#if $session.isAdmin}
<button
type="submit"
@@ -228,13 +226,17 @@
class:hover:bg-green-500={!loading}
class:hover:bg-orange-400={forceSave}
disabled={loading}
>{loading ? 'Saving...' : forceSave ? 'Are you sure to continue?' : 'Save'}</button
>{loading
? $t('forms.saving')
: forceSave
? $t('forms.confirm_continue')
: $t('forms.save')}</button
>
{/if}
</div>
<div class="grid grid-flow-row gap-2 px-10">
<div class="mt-2 grid grid-cols-2 items-center">
<label for="name" class="text-base font-bold text-stone-100">Name</label>
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
<input
readonly={!$session.isAdmin}
name="name"
@@ -244,7 +246,9 @@
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="gitSource" class="text-base font-bold text-stone-100">Git Source</label>
<label for="gitSource" class="text-base font-bold text-stone-100"
>{$t('application.git_source')}</label
>
<a
href={$session.isAdmin
? `/applications/${id}/configuration/source?from=/applications/${id}`
@@ -259,7 +263,9 @@
>
</div>
<div class="grid grid-cols-2 items-center">
<label for="repository" class="text-base font-bold text-stone-100">Git Repository</label>
<label for="repository" class="text-base font-bold text-stone-100"
>{$t('application.git_repository')}</label
>
<a
href={$session.isAdmin
? `/applications/${id}/configuration/repository?from=/applications/${id}&to=/applications/${id}/configuration/buildpack`
@@ -274,7 +280,9 @@
>
</div>
<div class="grid grid-cols-2 items-center">
<label for="buildPack" class="text-base font-bold text-stone-100">Build Pack</label>
<label for="buildPack" class="text-base font-bold text-stone-100"
>{$t('application.build_pack')}</label
>
<a
href={$session.isAdmin
? `/applications/${id}/configuration/buildpack?from=/applications/${id}`
@@ -290,7 +298,9 @@
>
</div>
<div class="grid grid-cols-2 items-center pb-8">
<label for="destination" class="text-base font-bold text-stone-100">Destination</label>
<label for="destination" class="text-base font-bold text-stone-100"
>{$t('application.destination')}</label
>
<div class="no-underline">
<input
value={application.destinationDocker.name}
@@ -302,20 +312,20 @@
</div>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">Application</div>
<div class="title">{$t('application.application')}</div>
</div>
<div class="grid grid-flow-row gap-2 px-10">
<div class="grid grid-cols-2">
<div class="flex-col">
<label for="fqdn" class="pt-2 text-base font-bold text-stone-100">URL (FQDN)</label>
<label for="fqdn" class="pt-2 text-base font-bold text-stone-100"
>{$t('application.url_fqdn')}</label
>
{#if browser && window.location.hostname === 'demo.coolify.io'}
<Explainer
text="<span class='text-white font-bold'>You can use the predefined random url name or enter your own domain name.</span>"
/>
{/if}
<Explainer
text="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 url, 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>"
/>
<Explainer text={$t('application.https_explainer')} />
</div>
<input
readonly={!$session.isAdmin || isRunning}
@@ -331,12 +341,12 @@
</div>
<div class="grid grid-cols-2 items-center pb-8">
<Setting
dataTooltip="Must be stopped to modify."
dataTooltip={$t('forms.must_be_stopped_to_modify')}
disabled={isRunning}
isCenter={false}
bind:setting={dualCerts}
title="Generate SSL for www and non-www?"
description="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."
title={$t('application.ssl_www_and_non_www')}
description={$t('application.ssl_explainer')}
on:click={() => !isRunning && changeSettings('dualCerts')}
/>
</div>
@@ -375,13 +385,13 @@
{/if}
{#if !staticDeployments.includes(application.buildPack)}
<div class="grid grid-cols-2 items-center">
<label for="port" class="text-base font-bold text-stone-100">Port</label>
<label for="port" class="text-base font-bold text-stone-100">{$t('forms.port')}</label>
<input
readonly={!$session.isAdmin}
name="port"
id="port"
bind:value={application.port}
placeholder={application.buildPack === 'python' ? '8000' : '3000'}
placeholder="{$t('forms.default')}: 'python' ? '8000' : '3000'"
/>
</div>
{/if}
@@ -389,63 +399,105 @@
{#if !notNodeDeployments.includes(application.buildPack)}
<div class="grid grid-cols-2 items-center">
<label for="installCommand" class="text-base font-bold text-stone-100"
>Install Command</label
>{$t('application.install_command')}</label
>
<input
readonly={!$session.isAdmin}
name="installCommand"
id="installCommand"
bind:value={application.installCommand}
placeholder="default: yarn install"
placeholder="{$t('forms.default')}: yarn install"
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="buildCommand" class="text-base font-bold text-stone-100">Build Command</label>
<label for="buildCommand" class="text-base font-bold text-stone-100"
>{$t('application.build_command')}</label
>
<input
readonly={!$session.isAdmin}
name="buildCommand"
id="buildCommand"
bind:value={application.buildCommand}
placeholder="default: yarn build"
placeholder="{$t('forms.default')}: yarn build"
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="startCommand" class="text-base font-bold text-stone-100">Start Command</label>
<label for="startCommand" class="text-base font-bold text-stone-100"
>{$t('application.start_command')}</label
>
<input
readonly={!$session.isAdmin}
name="startCommand"
id="startCommand"
bind:value={application.startCommand}
placeholder="default: yarn start"
placeholder="{$t('forms.default')}: yarn start"
/>
</div>
{/if}
{#if application.buildPack === 'docker'}
<div class="grid grid-cols-2 items-center">
<label for="dockerFileLocation" class="text-base font-bold text-stone-100"
>Dockerfile Location</label
>
<input
readonly={!$session.isAdmin}
name="dockerFileLocation"
id="dockerFileLocation"
bind:value={application.dockerFileLocation}
placeholder="default: /Dockerfile"
/>
<Explainer
text="Does not rely on Base Directory. <br>Should be absolute path, like <span class='text-green-500 font-bold'>/data/Dockerfile</span> or <span class='text-green-500 font-bold'>/Dockerfile.</span>"
/>
</div>
{/if}
{#if application.buildPack === 'deno'}
<div class="grid grid-cols-2 items-center">
<label for="denoMainFile" class="text-base font-bold text-stone-100">Main File</label>
<input
readonly={!$session.isAdmin}
name="denoMainFile"
id="denoMainFile"
bind:value={application.denoMainFile}
placeholder="default: main.ts"
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="denoOptions" class="text-base font-bold text-stone-100">Arguments</label>
<input
readonly={!$session.isAdmin}
name="denoOptions"
id="denoOptions"
bind:value={application.denoOptions}
placeholder="eg: --allow-net --allow-hrtime --config path/to/file.json"
/>
<Explainer
text="List of arguments to pass to <span class='text-green-500 font-bold'>deno run</span> command. Could include permissions, configurations files, etc."
/>
</div>
{/if}
<div class="grid grid-cols-2 items-center">
<div class="flex-col">
<label for="baseDirectory" class="pt-2 text-base font-bold text-stone-100"
>Base Directory</label
>{$t('forms.base_directory')}</label
>
<Explainer
text="Directory to use as the base for all commands.<br>Could be useful with <span class='text-green-500 font-bold'>monorepos</span>."
/>
<Explainer text={$t('application.directory_to_use_explainer')} />
</div>
<input
readonly={!$session.isAdmin}
name="baseDirectory"
id="baseDirectory"
bind:value={application.baseDirectory}
placeholder="default: /"
placeholder="{$t('forms.default')}: /"
/>
</div>
{#if !notNodeDeployments.includes(application.buildPack)}
<div class="grid grid-cols-2 items-center">
<div class="flex-col">
<label for="publishDirectory" class="pt-2 text-base font-bold text-stone-100"
>Publish Directory</label
>{$t('forms.publish_directory')}</label
>
<Explainer
text="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>."
/>
<Explainer text={$t('application.publish_directory_explainer')} />
</div>
<input
@@ -453,14 +505,14 @@
name="publishDirectory"
id="publishDirectory"
bind:value={application.publishDirectory}
placeholder=" default: /"
placeholder=" {$t('forms.default')}: /"
/>
</div>
{/if}
</div>
</form>
<div class="flex space-x-1 pb-5 font-bold">
<div class="title">Features</div>
<div class="title">{$t('application.features')}</div>
</div>
<div class="px-10 pb-10">
<div class="grid grid-cols-2 items-center">
@@ -468,8 +520,8 @@
isCenter={false}
bind:setting={autodeploy}
on:click={() => changeSettings('autodeploy')}
title="Enable Automatic Deployment"
description="Enable automatic deployment through webhooks."
title={$t('application.enable_automatic_deployment')}
description={$t('application.enable_auto_deploy_webhooks')}
/>
</div>
<div class="grid grid-cols-2 items-center">
@@ -477,8 +529,8 @@
isCenter={false}
bind:setting={previews}
on:click={() => changeSettings('previews')}
title="Enable MR/PR Previews"
description="Enable preview deployments from pull or merge requests."
title={$t('application.enable_mr_pr_previews')}
description={$t('application.enable_preview_deploy_mr_pr_requests')}
/>
</div>
<div class="grid grid-cols-2 items-center">
@@ -486,8 +538,8 @@
isCenter={false}
bind:setting={debug}
on:click={() => changeSettings('debug')}
title="Debug Logs"
description="Enable debug logs during build phase.<br><span class='text-red-500 font-bold'>Sensitive information</span> could be visible and saved in logs."
title={$t('application.debug_logs')}
description={$t('application.enable_debug_log_during_build')}
/>
</div>
</div>

View File

@@ -10,6 +10,7 @@
import LoadingLogs from '../_Loading.svelte';
import { get } from '$lib/api';
import { errorNotification } from '$lib/form';
import { t } from '$lib/translations';
let logs = [];
let loading = true;
@@ -84,7 +85,7 @@
<LoadingLogs />
{/if}
{#if currentStatus === 'queued'}
<div class="text-center font-bold text-xl">Queued and waiting for execution.</div>
<div class="text-center font-bold text-xl">{$t('application.build.queued_waiting_exec')}</div>
{:else}
<div class="flex justify-end sticky top-0 p-2">
<button

View File

@@ -21,12 +21,12 @@
<script lang="ts">
import { page } from '$app/stores';
import { changeQueryParams, dateOptions, getDomain } from '$lib/components/common';
import { changeQueryParams, dateOptions } from '$lib/components/common';
import BuildLog from './_BuildLog.svelte';
import { get } from '$lib/api';
import { errorNotification } from '$lib/form';
import { goto } from '$app/navigation';
import { t } from '$lib/translations';
export let builds;
export let application;
@@ -87,7 +87,9 @@
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">Build Logs</div>
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
{$t('application.build_logs')}
</div>
<span class="text-xs">{application.name} </span>
</div>
@@ -174,7 +176,7 @@
>
<div class="flex-col px-2">
<div class="text-sm font-bold">
{application.branch}
{build.branch || application.branch}
</div>
<div class="text-xs">
{build.type}
@@ -184,12 +186,14 @@
<div class="w-48 text-center text-xs">
{#if build.status === 'running'}
<div class="font-bold">Running</div>
<div class="font-bold">{$t('application.build.running')}</div>
{:else if build.status === 'queued'}
<div class="font-bold">Queued</div>
<div class="font-bold">{$t('application.build.queued')}</div>
{:else}
<div>{build.since}</div>
<div>Finished in <span class="font-bold">{build.took}s</span></div>
<div>
{$t('application.build.finished_in')} <span class="font-bold">{build.took}s</span>
</div>
{/if}
</div>
</div>
@@ -198,7 +202,8 @@
{#if !noMoreBuilds}
{#if buildCount > 5}
<div class="flex space-x-2">
<button disabled={noMoreBuilds} class="w-full" on:click={loadMoreBuilds}>Load More</button
<button disabled={noMoreBuilds} class="w-full" on:click={loadMoreBuilds}
>{$t('application.build.load_more')}</button
>
</div>
{/if}
@@ -213,5 +218,5 @@
</div>
</div>
{#if buildCount === 0}
<div class="text-center text-xl font-bold">No logs found</div>
<div class="text-center text-xl font-bold">{$t('application.build.no_logs')}</div>
{/if}

View File

@@ -10,6 +10,10 @@ export const get: RequestHandler = async (event) => {
if (status === 401) return { status, body };
const { id } = event.params;
let since = event.url.searchParams.get('since') || 0;
if (since !== 0) {
since = dayjs(since).unix();
}
try {
const { destinationDockerId, destinationDocker } = await db.prisma.application.findUnique({
where: { id },
@@ -20,13 +24,22 @@ export const get: RequestHandler = async (event) => {
try {
const container = await docker.engine.getContainer(id);
if (container) {
const logs = (
await container.logs({
stdout: true,
stderr: true,
timestamps: true,
since,
tail: 5000
})
)
.toString()
.split('\n')
.map((l) => l.slice(8))
.filter((a) => a);
return {
body: {
logs: (await container.logs({ stdout: true, stderr: true, timestamps: true }))
.toString()
.split('\n')
.map((l) => l.slice(8))
.filter((a) => a)
logs
}
};
}

View File

@@ -24,19 +24,21 @@
export let application;
import { page } from '$app/stores';
import LoadingLogs from './_Loading.svelte';
import { getDomain } from '$lib/components/common';
import { get } from '$lib/api';
import { errorNotification } from '$lib/form';
import { t } from '$lib/translations';
let loadLogsInterval = null;
let logs = [];
let followingBuild;
let lastLog = null;
let followingInterval;
let followingLogs;
let logsEl;
let position = 0;
const { id } = $page.params;
onMount(async () => {
loadLogs();
loadAllLogs();
loadLogsInterval = setInterval(() => {
loadLogs();
}, 1000);
@@ -45,25 +47,53 @@
clearInterval(loadLogsInterval);
clearInterval(followingInterval);
});
async function loadAllLogs() {
try {
const data: any = await get(`/applications/${id}/logs.json`);
if (data?.logs) {
lastLog = data.logs[data.logs.length - 1];
logs = data.logs;
}
} catch (error) {
console.log(error);
return errorNotification(error);
}
}
async function loadLogs() {
try {
const newLogs = await get(`/applications/${id}/logs.json`);
logs = newLogs.logs;
return;
} catch ({ error }) {
const newLogs: any = await get(
`/applications/${id}/logs.json?since=${lastLog?.split(' ')[0] || 0}`
);
if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) {
logs = logs.concat(newLogs.logs);
lastLog = newLogs.logs[newLogs.logs.length - 1];
}
} catch (error) {
return errorNotification(error);
}
}
function detect() {
if (position < logsEl.scrollTop) {
position = logsEl.scrollTop;
} else {
if (followingLogs) {
clearInterval(followingInterval);
followingLogs = false;
}
position = logsEl.scrollTop;
}
}
function followBuild() {
followingBuild = !followingBuild;
if (followingBuild) {
followingLogs = !followingLogs;
if (followingLogs) {
followingInterval = setInterval(() => {
logsEl.scrollTop = logsEl.scrollHeight;
window.scrollTo(0, document.body.scrollHeight);
}, 100);
}, 1000);
} else {
window.clearInterval(followingInterval);
clearInterval(followingInterval);
}
}
</script>
@@ -142,16 +172,19 @@
</div>
<div class="flex flex-row justify-center space-x-2 px-10 pt-6">
{#if logs.length === 0}
<div class="text-xl font-bold tracking-tighter">Waiting for the logs...</div>
<div class="text-xl font-bold tracking-tighter">{$t('application.build.waiting_logs')}</div>
{:else}
<div class="relative w-full">
<LoadingLogs />
<div class="flex justify-end sticky top-0 p-2">
<div class="text-right " />
{#if loadLogsInterval}
<LoadingLogs />
{/if}
<div class="flex justify-end sticky top-0 p-2 mx-1">
<button
on:click={followBuild}
class="bg-transparent"
data-tooltip="Follow logs"
class:text-green-500={followingBuild}
class:text-green-500={followingLogs}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -174,8 +207,9 @@
<div
class="font-mono w-full leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
bind:this={logsEl}
on:scroll={detect}
>
<div class="px-2">
<div class="px-2 pr-14">
{#each logs as log}
{log + '\n'}
{/each}

View File

@@ -30,6 +30,7 @@
import Explainer from '$lib/components/Explainer.svelte';
import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast';
import { t } from '$lib/translations';
const { id } = $page.params;
async function refreshSecrets() {
@@ -134,10 +135,12 @@
<table class="mx-auto border-separate text-left">
<thead>
<tr class="h-12">
<th scope="col">Name</th>
<th scope="col">Value</th>
<th scope="col" class="w-64 text-center">Need during buildtime?</th>
<th scope="col" class="w-96 text-center">Action</th>
<th scope="col">{$t('forms.name')}</th>
<th scope="col">{$t('forms.value')}</th>
<th scope="col" class="w-64 text-center"
>{$t('application.preview.need_during_buildtime')}</th
>
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th>
</tr>
</thead>
<tbody>
@@ -171,13 +174,15 @@
</a>
<div class="flex items-center justify-center">
<button class="bg-coollabs hover:bg-coollabs-100" on:click={() => redeploy(container)}
>Redeploy</button
>{$t('application.preview.redeploy')}</button
>
</div>
{/each}
{:else}
<div class="flex-col">
<div class="text-center font-bold text-xl">No previews available</div>
<div class="text-center font-bold text-xl">
{$t('application.preview.no_previews_available')}
</div>
</div>
{/if}
</div>

View File

@@ -6,6 +6,7 @@
import { saveSecret } from './utils';
import pLimit from 'p-limit';
import { createEventDispatcher } from 'svelte';
import { t } from '$lib/translations';
const dispatch = createEventDispatcher();
let batchSecrets = '';
@@ -38,11 +39,11 @@
}
</script>
<h2 class="title my-6 font-bold">Paste .env file</h2>
<h2 class="title my-6 font-bold">{$t('application.secret__batch_dot_env')}</h2>
<form on:submit|preventDefault={getValues} class="mb-12 w-full">
<textarea bind:value={batchSecrets} class="mb-2 min-h-[200px] w-full" />
<button
class="bg-green-600 hover:bg-green-500 disabled:text-white disabled:opacity-40"
type="submit">Batch add secrets</button
type="submit">{$t('application.batch_secrets')}</button
>
</form>

View File

@@ -12,6 +12,7 @@
import { del } from '$lib/api';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { errorNotification } from '$lib/form';
import { t } from '$lib/translations';
import { toast } from '@zerodevx/svelte-toast';
import { createEventDispatcher } from 'svelte';
import { saveSecret } from './utils';
@@ -104,7 +105,7 @@
class:cursor-not-allowed={isPRMRSecret}
class:cursor-pointer={!isPRMRSecret}
>
<span class="sr-only">Use isBuildSecret</span>
<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}
@@ -145,17 +146,19 @@
{#if isNewSecret}
<div class="flex items-center justify-center">
<button class="bg-green-600 hover:bg-green-500" on:click={() => createSecret(true)}
>Add</button
>{$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="" on:click={() => createSecret(false)}>Set</button>
<button class="" on:click={() => createSecret(false)}>{$t('forms.set')}</button>
</div>
{#if !isPRMRSecret}
<div class="flex justify-center items-end">
<button class="bg-red-600 hover:bg-red-500" on:click={removeSecret}>Remove</button>
<button class="bg-red-600 hover:bg-red-500" on:click={removeSecret}
>{$t('forms.remove')}</button
>
</div>
{/if}
</div>

View File

@@ -25,6 +25,7 @@
import pLimit from 'p-limit';
import Secret from './_Secret.svelte';
import { page } from '$app/stores';
import { t } from '$lib/translations';
import { get } from '$lib/api';
import { saveSecret } from './utils';
import { toast } from '@zerodevx/svelte-toast';
@@ -65,7 +66,9 @@
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">Secrets</div>
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
{$t('application.secret')}
</div>
<span class="text-xs">{application.name} </span>
</div>
@@ -137,10 +140,12 @@
<table class="mx-auto border-separate text-left">
<thead>
<tr class="h-12">
<th scope="col">Name</th>
<th scope="col">Value</th>
<th scope="col" class="w-64 text-center">Need during buildtime?</th>
<th scope="col" class="w-96 text-center">Action</th>
<th scope="col">{$t('forms.name')}</th>
<th scope="col">{$t('forms.value')}</th>
<th scope="col" class="w-64 text-center"
>{$t('application.preview.need_during_buildtime')}</th
>
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th>
</tr>
</thead>
<tbody>

View File

@@ -1,6 +1,7 @@
import { toast } from '@zerodevx/svelte-toast';
import { errorNotification } from '$lib/form';
import { post } from '$lib/api';
import { t } from '$lib/translations';
type Props = {
isNew: boolean;
@@ -21,8 +22,8 @@ export async function saveSecret({
isNewSecret,
applicationId
}: Props): Promise<void> {
if (!name) return errorNotification('Name is required.');
if (!value) return errorNotification('Value is required.');
if (!name) return errorNotification(`${t.get('forms.name')} ${t.get('forms.is_required')}`);
if (!value) return errorNotification(`${t.get('forms.value')} ${t.get('forms.is_required')}`);
try {
await post(`/applications/${applicationId}/secrets.json`, {
name,

View File

@@ -1,6 +1,7 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import { t } from '$lib/translations';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
@@ -14,8 +15,7 @@ export const post: RequestHandler = async (event) => {
const isDouble = await db.checkDoubleBranch(branch, projectId);
if (isDouble && autodeploy) {
throw {
message:
'Cannot activate automatic deployments until only one application is defined for this repository / branch.'
message: t.get('application.cant_activate_auto_deploy_without_repo')
};
}
await db.setApplicationSettings({ id, debug, previews, dualCerts, autodeploy });

View File

@@ -10,12 +10,13 @@
import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast';
import { t } from '$lib/translations';
const { id } = $page.params;
const dispatch = createEventDispatcher();
async function saveStorage(newStorage = false) {
try {
if (!storage.path) return errorNotification('Path is required.');
if (!storage.path) return errorNotification($t('application.storage.path_is_required'));
storage.path = storage.path.startsWith('/') ? storage.path : `/${storage.path}`;
storage.path = storage.path.endsWith('/') ? storage.path.slice(0, -1) : storage.path;
storage.path.replace(/\/\//g, '/');
@@ -29,8 +30,8 @@
storage.path = null;
storage.id = null;
}
if (newStorage) toast.push('Storage saved.');
else toast.push('Storage updated.');
if (newStorage) toast.push($t('application.storage.storage_saved'));
else toast.push($t('application.storage.storage_updated'));
} catch ({ error }) {
return errorNotification(error);
}
@@ -39,7 +40,7 @@
try {
await del(`/applications/${id}/storage.json`, { path: storage.path });
dispatch('refresh');
toast.push('Storage deleted.');
toast.push($t('application.storage.storage_deleted'));
} catch ({ error }) {
return errorNotification(error);
}
@@ -57,16 +58,19 @@
<td>
{#if isNew}
<div class="flex items-center justify-center">
<button class="bg-green-600 hover:bg-green-500" on:click={() => saveStorage(true)}>Add</button
<button class="bg-green-600 hover:bg-green-500" on:click={() => saveStorage(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="" on:click={() => saveStorage(false)}>Set</button>
<button class="" on:click={() => saveStorage(false)}>{$t('forms.set')}</button>
</div>
<div class="flex justify-center items-end">
<button class="bg-red-600 hover:bg-red-500" on:click={removeStorage}>Remove</button>
<button class="bg-red-600 hover:bg-red-500" on:click={removeStorage}
>{$t('forms.remove')}</button
>
</div>
</div>
{/if}

View File

@@ -28,6 +28,7 @@
import Storage from './_Storage.svelte';
import { get } from '$lib/api';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations';
const { id } = $page.params;
async function refreshStorage() {
@@ -111,15 +112,12 @@
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4">
<div class="flex justify-center py-4 text-center">
<Explainer
customClass="w-full"
text={'You can specify any folder that you want to be persistent across deployments. <br>This is useful for storing data such as a database (SQLite) or a cache.'}
/>
<Explainer customClass="w-full" text={$t('application.storage.persistent_storage_explainer')} />
</div>
<table class="mx-auto border-separate text-left">
<thead>
<tr class="h-12">
<th scope="col">Path</th>
<th scope="col">{$t('forms.path')}</th>
</tr>
</thead>
<tbody>

View File

@@ -3,6 +3,8 @@
import { session } from '$app/stores';
import { post } from '$lib/api';
import { goto } from '$app/navigation';
import { t } from '$lib/translations';
import { getDomain } from '$lib/components/common';
import Rust from '$lib/components/svg/applications/Rust.svelte';
import Nodejs from '$lib/components/svg/applications/Nodejs.svelte';
@@ -19,7 +21,7 @@
import Docker from '$lib/components/svg/applications/Docker.svelte';
import Astro from '$lib/components/svg/applications/Astro.svelte';
import Eleventy from '$lib/components/svg/applications/Eleventy.svelte';
import { getDomain } from '$lib/components/common';
import Deno from '$lib/components/svg/applications/Deno.svelte';
async function newApplication() {
const { id } = await post('/applications/new', {});
@@ -38,7 +40,7 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl ">Applications</div>
<div class="mr-4 text-2xl ">{$t('index.applications')}</div>
{#if $session.isAdmin}
<div on:click={newApplication} class="add-icon cursor-pointer bg-green-600 hover:bg-green-500">
<svg
@@ -60,7 +62,7 @@
<div class="flex flex-col flex-wrap justify-center">
{#if !applications || ownApplications.length === 0}
<div class="flex-col">
<div class="text-center text-xl font-bold">No applications found</div>
<div class="text-center text-xl font-bold">{$t('application.no_applications_found')}</div>
</div>
{/if}
{#if ownApplications.length > 0 || otherApplications.length > 0}
@@ -100,6 +102,8 @@
<Astro />
{:else if application.buildPack.toLowerCase() === 'eleventy'}
<Eleventy />
{:else if application.buildPack.toLowerCase() === 'deno'}
<Deno />
{/if}
{/if}
@@ -156,6 +160,8 @@
<Astro />
{:else if application.buildPack.toLowerCase() === 'eleventy'}
<Eleventy />
{:else if application.buildPack.toLowerCase() === 'deno'}
<Deno />
{/if}
{/if}

View File

@@ -57,7 +57,8 @@ export const post: RequestHandler = async (event) => {
headers: {
'set-cookie': [
`${cookie}=${value}; HttpOnly; Path=/; Max-Age=15778800;`,
'gitlabToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'
'gitlabToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT',
'githubToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'
],
Location: from
}

View File

@@ -1,6 +1,7 @@
<script>
export let database;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { t } from '$lib/translations';
</script>
<div class="flex space-x-1 py-5 font-bold">
@@ -8,34 +9,38 @@
</div>
<div class="space-y-2 px-10">
<div class="grid grid-cols-2 items-center">
<label for="defaultDatabase" class="text-base font-bold text-stone-100">Default Database</label>
<label for="defaultDatabase" class="text-base font-bold text-stone-100"
>{$t('database.default_database')}</label
>
<CopyPasswordField
required
readonly={database.defaultDatabase}
disabled={database.defaultDatabase}
placeholder="eg: mydb"
placeholder="{$t('forms.eg')}: mydb"
id="defaultDatabase"
name="defaultDatabase"
bind:value={database.defaultDatabase}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="dbUser" class="text-base font-bold text-stone-100">User</label>
<label for="dbUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
id="dbUser"
name="dbUser"
value={database.dbUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
<label for="dbUserPassword" class="text-base font-bold text-stone-100"
>{$t('forms.password')}</label
>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField
id="dbUserPassword"
name="dbUserPassword"
@@ -43,22 +48,24 @@
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100">Root User</label>
<label for="rootUser" class="text-base font-bold text-stone-100">{$t('forms.root_user')}</label>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
id="rootUser"
name="rootUser"
value={database.rootUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="rootUserPassword" class="text-base font-bold text-stone-100">Root's Password</label>
<label for="rootUserPassword" class="text-base font-bold text-stone-100"
>{$t('forms.roots_password')}</label
>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField
id="rootUserPassword"
name="rootUserPassword"

View File

@@ -18,6 +18,7 @@
import { post } from '$lib/api';
import { getDomain } from '$lib/components/common';
import { toast } from '@zerodevx/svelte-toast';
import { t } from '$lib/translations';
const { id } = $page.params;
@@ -59,7 +60,7 @@
: window.location.hostname
: database.id
}:${isPublic ? database.publicPort : privatePort}/${databaseDefault}`
: 'Loading...');
: $t('forms.loading'));
}
async function changeSettings(name) {
@@ -110,20 +111,20 @@
<div class="mx-auto max-w-4xl px-6">
<form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex space-x-1 pb-5 font-bold">
<div class="title">General</div>
<div class="title">{$t('general')}</div>
{#if $session.isAdmin}
<button
type="submit"
class:bg-purple-600={!loading}
class:hover:bg-purple-500={!loading}
disabled={loading}>{loading ? 'Saving...' : 'Save'}</button
disabled={loading}>{loading ? $t('forms.saving') : $t('forms.save')}</button
>
{/if}
</div>
<div class="grid grid-flow-row gap-2 px-10">
<div class="grid grid-cols-2 items-center">
<label for="name" class="text-base font-bold text-stone-100">Name</label>
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
<input
readonly={!$session.isAdmin}
name="name"
@@ -133,7 +134,9 @@
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="destination" class="text-base font-bold text-stone-100">Destination</label>
<label for="destination" class="text-base font-bold text-stone-100"
>{$t('application.destination')}</label
>
{#if database.destinationDockerId}
<div class="no-underline">
<input
@@ -148,16 +151,17 @@
</div>
<div class="grid grid-cols-2 items-center">
<label for="version" class="text-base font-bold text-stone-100">Version</label>
<label for="version" class="text-base font-bold text-stone-100">{$t('forms.version')}</label
>
<input value={database.version} readonly disabled class="bg-transparent " />
</div>
</div>
<div class="grid grid-flow-row gap-2 px-10 pt-2">
<div class="grid grid-cols-2 items-center">
<label for="host" class="text-base font-bold text-stone-100">Host</label>
<label for="host" class="text-base font-bold text-stone-100">{$t('forms.host')}</label>
<CopyPasswordField
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField={false}
readonly
disabled
@@ -167,9 +171,10 @@
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="publicPort" class="text-base font-bold text-stone-100">Port</label>
<label for="publicPort" class="text-base font-bold text-stone-100">{$t('forms.port')}</label
>
<CopyPasswordField
placeholder="Generated automatically after set to public"
placeholder={$t('database.generated_automatically_after_set_to_public')}
id="publicPort"
readonly
disabled
@@ -191,10 +196,12 @@
<CouchDb {database} />
{/if}
<div class="grid grid-cols-2 items-center px-10 pb-8">
<label for="url" class="text-base font-bold text-stone-100">Connection String</label>
<label for="url" class="text-base font-bold text-stone-100"
>{$t('database.connection_string')}</label
>
<CopyPasswordField
textarea={true}
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField={false}
id="url"
name="url"
@@ -206,7 +213,7 @@
</div>
</form>
<div class="flex space-x-1 pb-5 font-bold">
<div class="title">Features</div>
<div class="title">{$t('application.features')}</div>
</div>
<div class="px-10 pb-10">
<div class="grid grid-cols-2 items-center">
@@ -214,8 +221,8 @@
loading={publicLoading}
bind:setting={isPublic}
on:click={() => changeSettings('isPublic')}
title="Set it public"
description="Your database will be reachable over the internet. <br>Take security seriously in this case!"
title={$t('database.set_public')}
description={$t('database.warning_database_public')}
disabled={!isRunning}
/>
</div>
@@ -224,8 +231,8 @@
<Setting
bind:setting={appendOnly}
on:click={() => changeSettings('appendOnly')}
title="Change append only mode"
description="Useful if you would like to restore redis data from a backup.<br><span class='font-bold text-white'>Database restart is required.</span>"
title={$t('database.change_append_only_mode')}
description={$t('database.warning_append_only')}
/>
</div>
{/if}

View File

@@ -3,6 +3,7 @@
export let isRunning;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations';
</script>
<div class="flex space-x-1 py-5 font-bold">
@@ -10,9 +11,9 @@
</div>
<div class="space-y-2 px-10">
<div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100">Root User</label>
<label for="rootUser" class="text-base font-bold text-stone-100">{$t('forms.root_user')}</label>
<CopyPasswordField
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
id="rootUser"
readonly
disabled
@@ -21,11 +22,13 @@
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="rootUserPassword" class="text-base font-bold text-stone-100">Root's Password</label>
<label for="rootUserPassword" class="text-base font-bold text-stone-100"
>{$t('forms.roots_password')}</label
>
<CopyPasswordField
disabled={!isRunning}
readonly={!isRunning}
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField={true}
id="rootUserPassword"
name="rootUserPassword"

View File

@@ -3,6 +3,7 @@
export let isRunning;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations';
</script>
<div class="flex space-x-1 py-5 font-bold">
@@ -10,34 +11,38 @@
</div>
<div class="space-y-2 px-10">
<div class="grid grid-cols-2 items-center">
<label for="defaultDatabase" class="text-base font-bold text-stone-100">Default Database</label>
<label for="defaultDatabase" class="text-base font-bold text-stone-100"
>{$t('database.default_database')}</label
>
<CopyPasswordField
required
readonly={database.defaultDatabase}
disabled={database.defaultDatabase}
placeholder="eg: mydb"
placeholder="{$t('forms.eg')}: mydb"
id="defaultDatabase"
name="defaultDatabase"
bind:value={database.defaultDatabase}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="dbUser" class="text-base font-bold text-stone-100">User</label>
<label for="dbUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
id="dbUser"
name="dbUser"
value={database.dbUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
<label for="dbUserPassword" class="text-base font-bold text-stone-100"
>{$t('forms.password')}</label
>
<CopyPasswordField
disabled={!isRunning}
readonly={!isRunning}
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField
id="dbUserPassword"
name="dbUserPassword"
@@ -46,22 +51,24 @@
<Explainer text="Could be changed while the database is running." />
</div>
<div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100">Root User</label>
<label for="rootUser" class="text-base font-bold text-stone-100">{$t('forms.root_user')}</label>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
id="rootUser"
name="rootUser"
value={database.rootUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="rootUserPassword" class="text-base font-bold text-stone-100">Root's Password</label>
<label for="rootUserPassword" class="text-base font-bold text-stone-100"
>{$t('forms.roots_password')}</label
>
<CopyPasswordField
disabled={!isRunning}
readonly={!isRunning}
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField
id="rootUserPassword"
name="rootUserPassword"

View File

@@ -3,6 +3,7 @@
export let isRunning;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations';
</script>
<div class="flex space-x-1 py-5 font-bold">
@@ -10,12 +11,14 @@
</div>
<div class="space-y-2 px-10">
<div class="grid grid-cols-2 items-center">
<label for="defaultDatabase" class="text-base font-bold text-stone-100">Default Database</label>
<label for="defaultDatabase" class="text-base font-bold text-stone-100"
>{$t('database.default_database')}</label
>
<CopyPasswordField
required
readonly={database.defaultDatabase}
disabled={database.defaultDatabase}
placeholder="eg: mydb"
placeholder="{$t('forms.eg')}: mydb"
id="defaultDatabase"
name="defaultDatabase"
bind:value={database.defaultDatabase}
@@ -29,28 +32,32 @@
disabled={!isRunning}
readonly={!isRunning}
placeholder="Generated automatically after start"
isPasswordField
id="rootUserPassword"
name="rootUserPassword"
bind:value={database.rootUserPassword}
/>
<Explainer text="Could be changed while the database is running." />
</div>
<div class="grid grid-cols-2 items-center">
<label for="dbUser" class="text-base font-bold text-stone-100">User</label>
<label for="dbUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
id="dbUser"
name="dbUser"
value={database.dbUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
<label for="dbUserPassword" class="text-base font-bold text-stone-100"
>{$t('forms.password')}</label
>
<CopyPasswordField
disabled={!isRunning}
readonly={!isRunning}
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField
id="dbUserPassword"
name="dbUserPassword"

View File

@@ -3,6 +3,7 @@
export let isRunning;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations';
</script>
<div class="flex space-x-1 py-5 font-bold">
@@ -10,11 +11,13 @@
</div>
<div class="space-y-2 px-10">
<div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
<label for="dbUserPassword" class="text-base font-bold text-stone-100"
>{$t('forms.password')}</label
>
<CopyPasswordField
disabled={!isRunning}
readonly={!isRunning}
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField
id="dbUserPassword"
name="dbUserPassword"

View File

@@ -63,6 +63,7 @@
import Loading from '$lib/components/Loading.svelte';
import { del, post } from '$lib/api';
import { goto } from '$app/navigation';
import { t } from '$lib/translations';
export let database;
export let isRunning;
@@ -83,7 +84,7 @@
}
}
async function stopDatabase() {
const sure = confirm(`Are you sure you would like to stop '${database.name}'?`);
const sure = confirm($t('database.confirm_stop', { name: database.name }));
if (sure) {
loading = true;
try {
@@ -113,13 +114,13 @@
{#if isRunning}
<button
on:click={stopDatabase}
title="Stop database"
title={$t('database.stop_database')}
type="submit"
disabled={!$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-red-500"
data-tooltip={$session.isAdmin
? 'Stop database'
: 'You do not have permission to stop the database.'}
? $t('database.stop_database')
: $t('database.permission_denied_stop_database')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -139,13 +140,13 @@
{:else}
<button
on:click={startDatabase}
title="Start database"
title={$t('database.start_database')}
type="submit"
disabled={!$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-green-500"
data-tooltip={$session.isAdmin
? 'Start database'
: 'You do not have permission to start the database.'}
? $t('database.start_database')
: $t('database.permission_denied_start_database')}
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
@@ -164,14 +165,14 @@
{/if}
<button
on:click={deleteDatabase}
title="Delete Database"
title={$t('database.delete_database')}
type="submit"
disabled={!$session.isAdmin}
class:hover:text-red-500={$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm"
data-tooltip={$session.isAdmin
? 'Delete Database'
: 'You do not have permission to delete a Database'}><DeleteIcon /></button
? $t('database.delete_database')
: $t('database.permission_denied_delete_database')}><DeleteIcon /></button
>
{/if}
</nav>

View File

@@ -34,6 +34,7 @@
import { errorNotification } from '$lib/form';
import { goto } from '$app/navigation';
import { post } from '$lib/api';
import { t } from '$lib/translations';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
@@ -52,12 +53,14 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Configure Destination</div>
<div class="mr-4 text-2xl tracking-tight">
{$t('application.configuration.configure_destination')}
</div>
</div>
<div class="flex justify-center">
{#if !destinations || destinations.length === 0}
<div class="flex-col">
<div class="pb-2">No configurable Destination found</div>
<div class="pb-2">{$t('application.configuration.no_configurable_destination')}</div>
<div class="flex justify-center">
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
<svg

View File

@@ -42,6 +42,7 @@
import Redis from '$lib/components/svg/databases/Redis.svelte';
import { goto } from '$app/navigation';
import { post } from '$lib/api';
import { t } from '$lib/translations';
async function handleSubmit(type) {
try {
await post(`/databases/${id}/configuration/type.json`, { type });
@@ -53,7 +54,7 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Select a Database type</div>
<div class="mr-4 text-2xl tracking-tight">{$t('database.select_database_type')}</div>
</div>
<div class="flex flex-wrap justify-center">

View File

@@ -31,6 +31,7 @@
import { enhance, errorNotification } from '$lib/form';
import { goto } from '$app/navigation';
import { post } from '$lib/api';
import { t } from '$lib/translations';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
@@ -47,7 +48,7 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Select a Database version</div>
<div class="mr-4 text-2xl tracking-tight">{$t('database.select_database_version')}</div>
</div>
<div class="flex flex-wrap justify-center">

View File

@@ -1,7 +1,7 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler, stopDatabase } from '$lib/database';
import { deleteProxy } from '$lib/haproxy';
import { stopTcpHttpProxy } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit';
export const del: RequestHandler = async (event) => {
@@ -12,7 +12,7 @@ export const del: RequestHandler = async (event) => {
const database = await db.getDatabase({ id, teamId });
if (database.destinationDockerId) {
const everStarted = await stopDatabase(database);
if (everStarted) await deleteProxy({ id });
if (everStarted) await stopTcpHttpProxy(database.destinationDocker, database.publicPort);
}
await db.removeDatabase({ id });
return { status: 200 };

View File

@@ -1,20 +1,16 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { generateDatabaseConfiguration, ErrorHandler } from '$lib/database';
import { generateDatabaseConfiguration, ErrorHandler, getFreePort } from '$lib/database';
import { startTcpProxy, stopTcpHttpProxy } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit';
import getPort, { portNumbers } from 'get-port';
export const post: RequestHandler = async (event) => {
const { status, body, teamId } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
const data = await db.prisma.setting.findFirst();
const { minPort, maxPort } = data;
const { isPublic, appendOnly = true } = await event.request.json();
const publicPort = await getPort({ port: portNumbers(minPort, maxPort) });
const publicPort = await getFreePort();
try {
await db.setDatabase({ id, isPublic, appendOnly });

View File

@@ -45,7 +45,15 @@ export const post: RequestHandler = async (event) => {
volumes: [volume],
ulimits,
labels,
restart: 'always'
restart: 'always',
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
}
},
networks: {

View File

@@ -9,6 +9,7 @@
import { post } from '$lib/api';
import { goto } from '$app/navigation';
import { session } from '$app/stores';
import { t } from '$lib/translations';
async function newDatabase() {
const { id } = await post('/databases/new', {});
@@ -27,7 +28,7 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Databases</div>
<div class="mr-4 text-2xl tracking-tight">{$t('index.databases')}</div>
<div on:click={newDatabase} class="add-icon cursor-pointer bg-purple-600 hover:bg-purple-500">
<svg
class="w-6"
@@ -48,7 +49,7 @@
<div class="flex flex-col flex-wrap justify-center">
{#if !databases || ownDatabases.length === 0}
<div class="flex-col">
<div class="text-center text-xl font-bold">No databases found</div>
<div class="text-center text-xl font-bold">{$t('database.no_databases_found')}</div>
</div>
{/if}
{#if ownDatabases.length > 0 || otherDatabases.length > 0}
@@ -78,7 +79,7 @@
{/if}
{#if !database.type}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Configuration missing
{$t('application.configuration.configuration_missing')}
</div>
{/if}
</div>

View File

@@ -2,6 +2,7 @@
export let app;
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { t } from '$lib/translations';
const { id } = $page.params;
let loading = true;
async function checkApp() {
@@ -58,20 +59,20 @@
<div class="box-selection hover:border-transparent hover:bg-coolgray-200">
<div class="truncate pb-2 text-center text-xl font-bold">{app.domain}</div>
{#if loading}
<div class="w-full text-center font-bold">Loading...</div>
<div class="w-full text-center font-bold">{$t('forms.loading')}</div>
{:else if app.foundByDomain}
<div class="w-full bg-coolgray-200 text-xs">
<span class="text-red-500">Domain</span> already used for
{@html $t('forms.already_used_for', { type: 'Domains' })}
<span class="text-red-500">{app.foundName}</span>
</div>
{:else if app.foundByRepository}
<div class="w-full bg-coolgray-200 text-xs">
<span class="text-red-500">Repository</span> already used for
{@html $t('forms.already_used_for', { type: 'Repository' })}
<span class="text-red-500">{app.foundName}</span>
</div>
{:else}
<button class="bg-green-600 hover:bg-green-500 w-full" on:click={addToCoolify}
>Add to Coolify</button
>{$t('destination.add_to_coolify')}</button
>
{/if}
</div>

View File

@@ -10,6 +10,7 @@
import { post } from '$lib/api';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { onMount } from 'svelte';
import { t } from '$lib/translations';
const { id } = $page.params;
let cannotDisable = settings.fqdn && destination.engine === '/var/run/docker.sock';
let loading = false;
@@ -85,7 +86,7 @@
async function stopProxy() {
try {
await post(`/destinations/${id}/stop.json`, { engine: destination.engine });
return toast.push('Coolify Proxy stopped!');
return toast.push($t('destination.coolify_proxy_stopped'));
} catch ({ error }) {
return errorNotification(error);
}
@@ -93,19 +94,17 @@
async function startProxy() {
try {
await post(`/destinations/${id}/start.json`, { engine: destination.engine });
return toast.push('Coolify Proxy started!');
return toast.push($t('destination.coolify_proxy_started'));
} catch ({ error }) {
return errorNotification(error);
}
}
async function forceRestartProxy() {
const sure = confirm(
'Are you sure you want to restart the proxy? Everything will be reconfigured in ~10 secs.'
);
const sure = confirm($t('destination.confirm_restart_proxy'));
if (sure) {
try {
restarting = true;
toast.push('Coolify Proxy restarting...');
toast.push($t('destination.coolify_proxy_restarting'));
await post(`/destinations/${id}/restart.json`, {
engine: destination.engine,
fqdn: settings.fqdn
@@ -123,7 +122,7 @@
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div class="flex space-x-1 pb-5">
<div class="title font-bold">Configuration</div>
<div class="title font-bold">{$t('forms.configuration')}</div>
{#if $session.isAdmin}
<button
type="submit"
@@ -131,13 +130,15 @@
class:bg-sky-600={!loading}
class:hover:bg-sky-500={!loading}
disabled={loading}
>{loading ? 'Saving...' : 'Save'}
>{loading ? $t('forms.saving') : $t('forms.save')}
</button>
<button
class={restarting ? '' : 'bg-red-600 hover:bg-red-500'}
disabled={restarting}
on:click|preventDefault={forceRestartProxy}
>{restarting ? 'Restarting... please wait...' : 'Force restart proxy'}</button
>{restarting
? $t('destination.restarting_please_wait')
: $t('destination.force_restart_proxy')}</button
>
{/if}
<!-- <button type="button" class="bg-coollabs hover:bg-coollabs-100" on:click={scanApps}
@@ -145,10 +146,10 @@
> -->
</div>
<div class="grid grid-cols-2 items-center px-10 ">
<label for="name" class="text-base font-bold text-stone-100">Name</label>
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
<input
name="name"
placeholder="name"
placeholder={$t('forms.name')}
disabled={!$session.isAdmin}
readonly={!$session.isAdmin}
bind:value={destination.name}
@@ -156,13 +157,13 @@
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="engine" class="text-base font-bold text-stone-100">Engine</label>
<label for="engine" class="text-base font-bold text-stone-100">{$t('forms.engine')}</label>
<CopyPasswordField
id="engine"
readonly
disabled
name="engine"
placeholder="eg: /var/run/docker.sock"
placeholder="{$t('forms.eg')}: /var/run/docker.sock"
value={destination.engine}
/>
</div>
@@ -171,13 +172,13 @@
<input name="remoteEngine" type="checkbox" bind:checked={payload.remoteEngine} />
</div> -->
<div class="grid grid-cols-2 items-center px-10">
<label for="network" class="text-base font-bold text-stone-100">Network</label>
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label>
<CopyPasswordField
id="network"
readonly
disabled
name="network"
placeholder="default: coolify"
placeholder="{$t('forms.default')}: coolify"
value={destination.network}
/>
</div>
@@ -188,7 +189,7 @@
disabled={cannotDisable}
bind:setting={destination.isCoolifyProxyUsed}
on:click={changeProxySetting}
title="Use Coolify Proxy?"
title={$t('destination.use_coolify_proxy')}
description={`This will install a proxy on the destination to allow you to access your applications and services without any manual configuration. Databases will have their own proxy. <br><br>${
cannotDisable
? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>'

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