Compare commits

...

87 Commits

Author SHA1 Message Date
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
Andras Bacsai
23e8833208 Merge pull request #339 from coollabsio/v2.4.5
v2.4.5
2022-04-12 19:08:46 +02:00
Andras Bacsai
03962663c2 fix: Timeout values 2022-04-12 18:21:10 +02:00
Andras Bacsai
cc2ec55c4d chore: version++ 2022-04-12 16:50:13 +02:00
Andras Bacsai
ff2c38aa16 fix: Invitations 2022-04-12 16:49:59 +02:00
Andras Bacsai
b5a9a2cea8 fix: Types 2022-04-12 16:49:52 +02:00
Andras Bacsai
cd3f661f7e Merge pull request #336 from coollabsio/v2.4.4
v2.4.4
2022-04-12 11:02:35 +02:00
Andras Bacsai
41bf6b5b86 fixes 2022-04-12 10:47:53 +02:00
Andras Bacsai
a4e7c85184 Add only amd release 2022-04-12 10:14:18 +02:00
Andras Bacsai
19aca9ab35 chore: version++ 2022-04-12 10:13:19 +02:00
Andras Bacsai
08704c289a fix: Proxy 2022-04-12 10:12:46 +02:00
Andras Bacsai
2224c22c6e fix: haproxy build stuffs 2022-04-12 09:22:27 +02:00
Andras Bacsai
b281889acd Merge branch 'main' of github.com:coollabsio/coolify into main 2022-04-12 09:20:12 +02:00
Andras Bacsai
cfc50a27b0 Package.json update 2022-04-12 09:19:48 +02:00
95 changed files with 1987 additions and 1002 deletions

View File

@@ -1,12 +1,12 @@
# 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 # Technical skills required
- Node.js / Javascript - Node.js / Javascript
- Svelte / SvelteKit - Svelte / SvelteKit
- Prisma.io - Prisma.io / SQL
# Recommended Pull Request Guideline # Recommended Pull Request Guideline
@@ -16,11 +16,13 @@ First of all, thank you for considering to contribute to my project! It means a
- Push to your fork repo - Push to your fork repo
- Create a pull request: https://github.com/coollabsio/compare - Create a pull request: https://github.com/coollabsio/compare
- Write a proper description - 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? # 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. You need to have [Docker Engine](https://docs.docker.com/engine/install/) installed locally.
@@ -41,4 +43,75 @@ If the schema is finalized, you need to create a migration file with `pnpm db:mi
## 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
I use MinIO as an example.
You need to add a new folder to [src/routes/services/[id]](src/routes/services/[id]) with the low-capital name of the service. It should have three files with the following properties:
1. `index.json.ts`: A POST endpoint that updates Coolify's database about the service.
Basic services only require updating the URL(fqdn) and the name of the service.
2. `start.json.ts`: A start endpoint that setups the docker-compose file (for Local Docker Engines) and starts the service.
- To start a service, you need to know Coolify supported images and tags of the service. For that you need to update `supportedServiceTypesAndVersions` function at [src/lib/components/common.ts](src/lib/components/common.ts).
Example JSON:
```js
{
// Name used to identify the service in Coolify
name: 'minio',
// Fancier name to show to the user
fancyName: 'MinIO',
// Docker base image for the service
baseImage: 'minio/minio',
// Usable tags
versions: ['latest'],
// Which tag is the recommended
recommendedVersion: 'latest',
// Application's default port, MinIO listens on 9001 (and 9000, more details later on)
ports: {
main: 9001
}
},
```
- You need to define a compose file as `const composeFile: ComposeFile` found in [src/routes/services/[id]/minio/start.json.ts](src/routes/services/[id]/minio/start.json.ts)
**IMPORTANT:** It should contain `all the default environment variables` that are required for the service to function correctly and `all the volumes to persist data` in restarts.
- 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))
3. `stop.json.ts` A stop endpoint that stops the service.
It needs to stop all the services by their container name and proxies (if applicable).
4. You need to add the automatically generated variables (passwords, users, etc.) for the new service at [src/lib/database/services.ts](src/lib/database/services.ts), `configureServiceType` function.
## 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` and [src/lib/components/ServiceLinks.svelte](src/lib/components/ServiceLinks.svelte) with a link to the docs/main site of the service.
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)

View File

@@ -22,7 +22,7 @@ If you would like no questions during installation
### Git Sources ### 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 - Github
- GitLab - GitLab
@@ -38,7 +38,7 @@ You can deploy your applications to the following destinations:
### Applications ### 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 - Static sites
- NodeJS - 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

@@ -1,6 +1,6 @@
FROM haproxytech/haproxy-alpine:2.5 FROM haproxytech/haproxy-alpine:2.5
RUN mkdir -p /usr/local/etc/haproxy/ssl /usr/local/etc/haproxy/maps /usr/local/etc/haproxy/spoe RUN mkdir -p /usr/local/etc/haproxy/ssl /usr/local/etc/haproxy/maps /usr/local/etc/haproxy/spoe
COPY haproxy/haproxy.cfg-http.template /usr/local/etc/haproxy/haproxy.cfg COPY data/haproxy/haproxy.cfg-http.template /usr/local/etc/haproxy/haproxy.cfg
COPY haproxy/dataplaneapi.hcl /usr/local/etc/haproxy/dataplaneapi.hcl COPY data/haproxy/dataplaneapi.hcl /usr/local/etc/haproxy/dataplaneapi.hcl
COPY haproxy/ssl/default.pem /usr/local/etc/haproxy/ssl/default.pem COPY data/haproxy/ssl/default.pem /usr/local/etc/haproxy/ssl/default.pem

View File

@@ -1,6 +1,6 @@
FROM haproxytech/haproxy-alpine:2.5 FROM haproxytech/haproxy-alpine:2.5
RUN mkdir -p /usr/local/etc/haproxy/ssl /usr/local/etc/haproxy/maps /usr/local/etc/haproxy/spoe RUN mkdir -p /usr/local/etc/haproxy/ssl /usr/local/etc/haproxy/maps /usr/local/etc/haproxy/spoe
COPY haproxy/haproxy.cfg-tcp.template /usr/local/etc/haproxy/haproxy.cfg COPY data/haproxy/haproxy.cfg-tcp.template /usr/local/etc/haproxy/haproxy.cfg
COPY haproxy/dataplaneapi.hcl /usr/local/etc/haproxy/dataplaneapi.hcl COPY data/haproxy/dataplaneapi.hcl /usr/local/etc/haproxy/dataplaneapi.hcl
COPY haproxy/ssl/default.pem /usr/local/etc/haproxy/ssl/default.pem COPY data/haproxy/ssl/default.pem /usr/local/etc/haproxy/ssl/default.pem

View File

@@ -1,6 +1,6 @@
FROM haproxytech/haproxy-alpine:2.5 FROM haproxytech/haproxy-alpine:2.5
RUN mkdir -p /usr/local/etc/haproxy/ssl /usr/local/etc/haproxy/maps /usr/local/etc/haproxy/spoe RUN mkdir -p /usr/local/etc/haproxy/ssl /usr/local/etc/haproxy/maps /usr/local/etc/haproxy/spoe
COPY haproxy/haproxy.cfg.template /usr/local/etc/haproxy/haproxy.cfg COPY data/haproxy/haproxy.cfg.template /usr/local/etc/haproxy/haproxy.cfg
COPY haproxy/dataplaneapi.hcl /usr/local/etc/haproxy/dataplaneapi.hcl COPY data/haproxy/dataplaneapi.hcl /usr/local/etc/haproxy/dataplaneapi.hcl
COPY haproxy/ssl/default.pem /usr/local/etc/haproxy/ssl/default.pem COPY data/haproxy/ssl/default.pem /usr/local/etc/haproxy/ssl/default.pem

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 WORKDIR /prisma
ENV RUSTFLAGS="-C target-feature=-crt-static" ENV RUSTFLAGS="-C target-feature=-crt-static"
RUN apk --no-cache add openssl direnv git musl-dev openssl-dev build-base perl protoc 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 RUN cargo build --release
FROM alpine FROM alpine

View File

@@ -2,10 +2,8 @@ version: '3.8'
services: services:
redis: redis:
image: 'bitnami/redis:6.2' image: redis:6.2-alpine
container_name: coolify-redis container_name: coolify-redis
environment:
- ALLOW_EMPTY_PASSWORD=yes
networks: networks:
- coolify-infra - coolify-infra
ports: ports:
@@ -13,7 +11,24 @@ services:
published: 6379 published: 6379
protocol: tcp protocol: tcp
mode: host 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: networks:
coolify-infra: coolify-infra:
attachable: true attachable: true

View File

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

View File

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

254
pnpm-lock.yaml generated
View File

@@ -5,23 +5,23 @@ specifiers:
'@prisma/client': 3.11.1 '@prisma/client': 3.11.1
'@sentry/node': 6.19.6 '@sentry/node': 6.19.6
'@sveltejs/adapter-node': 1.0.0-next.73 '@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-cookie': 3.0.1
'@types/js-yaml': 4.0.5 '@types/js-yaml': 4.0.5
'@types/node': 17.0.23 '@types/node': 17.0.25
'@types/node-forge': 1.0.1 '@types/node-forge': 1.0.1
'@typescript-eslint/eslint-plugin': 4.31.1 '@typescript-eslint/eslint-plugin': 4.31.1
'@typescript-eslint/parser': 4.31.1 '@typescript-eslint/parser': 4.31.1
'@zerodevx/svelte-toast': 0.7.1 '@zerodevx/svelte-toast': 0.7.1
autoprefixer: 10.4.4 autoprefixer: 10.4.4
bcryptjs: ^2.4.3 bcryptjs: 2.4.3
bullmq: 1.79.0 bullmq: 1.80.4
compare-versions: 4.1.3 compare-versions: 4.1.3
cookie: 0.4.2 cookie: 0.5.0
cross-env: 7.0.3 cross-env: 7.0.3
cross-var: 1.1.0 cross-var: 1.1.0
cuid: 2.1.8 cuid: 2.1.8
dayjs: 1.11.0 dayjs: 1.11.1
dockerode: 3.3.1 dockerode: 3.3.1
dotenv-extended: 2.9.0 dotenv-extended: 2.9.0
eslint: 7.32.0 eslint: 7.32.0
@@ -34,21 +34,21 @@ specifiers:
js-cookie: 3.0.1 js-cookie: 3.0.1
js-yaml: 4.1.0 js-yaml: 4.1.0
jsonwebtoken: 8.5.1 jsonwebtoken: 8.5.1
lint-staged: 12.3.7 lint-staged: 12.4.0
mustache: 4.2.0 mustache: 4.2.0
node-forge: 1.3.1 node-forge: 1.3.1
p-limit: 4.0.0 p-limit: 4.0.0
postcss: 8.4.12 postcss: 8.4.12
prettier: 2.6.2 prettier: 2.6.2
prettier-plugin-svelte: 2.7.0 prettier-plugin-svelte: 2.7.0
prettier-plugin-tailwindcss: 0.1.8 prettier-plugin-tailwindcss: 0.1.10
prisma: 3.11.1 prisma: 3.11.1
svelte: 3.47.0 svelte: 3.47.0
svelte-check: 2.6.0 svelte-check: 2.7.0
svelte-kit-cookie-session: 2.1.2 svelte-kit-cookie-session: 2.1.3
svelte-preprocess: 4.10.5 svelte-preprocess: 4.10.6
svelte-select: 4.4.7 svelte-select: 4.4.7
tailwindcss: 3.0.23 tailwindcss: 3.0.24
tailwindcss-scrollbar: 0.1.0 tailwindcss-scrollbar: 0.1.0
ts-node: 10.7.0 ts-node: 10.7.0
tslib: 2.3.1 tslib: 2.3.1
@@ -60,11 +60,11 @@ dependencies:
'@prisma/client': 3.11.1_prisma@3.11.1 '@prisma/client': 3.11.1_prisma@3.11.1
'@sentry/node': 6.19.6 '@sentry/node': 6.19.6
bcryptjs: 2.4.3 bcryptjs: 2.4.3
bullmq: 1.79.0 bullmq: 1.80.4
compare-versions: 4.1.3 compare-versions: 4.1.3
cookie: 0.4.2 cookie: 0.5.0
cuid: 2.1.8 cuid: 2.1.8
dayjs: 1.11.0 dayjs: 1.11.1
dockerode: 3.3.1 dockerode: 3.3.1
dotenv-extended: 2.9.0 dotenv-extended: 2.9.0
generate-password: 1.7.0 generate-password: 1.7.0
@@ -76,16 +76,16 @@ dependencies:
mustache: 4.2.0 mustache: 4.2.0
node-forge: 1.3.1 node-forge: 1.3.1
p-limit: 4.0.0 p-limit: 4.0.0
svelte-kit-cookie-session: 2.1.2 svelte-kit-cookie-session: 2.1.3
tailwindcss-scrollbar: 0.1.0_tailwindcss@3.0.23 tailwindcss-scrollbar: 0.1.0_tailwindcss@3.0.24
unique-names-generator: 4.7.1 unique-names-generator: 4.7.1
devDependencies: devDependencies:
'@sveltejs/adapter-node': 1.0.0-next.73 '@sveltejs/adapter-node': 1.0.0-next.73
'@sveltejs/kit': 1.0.0-next.310_svelte@3.47.0 '@sveltejs/kit': 1.0.0-next.316_svelte@3.47.0
'@types/js-cookie': 3.0.1 '@types/js-cookie': 3.0.1
'@types/js-yaml': 4.0.5 '@types/js-yaml': 4.0.5
'@types/node': 17.0.23 '@types/node': 17.0.25
'@types/node-forge': 1.0.1 '@types/node-forge': 1.0.1
'@typescript-eslint/eslint-plugin': 4.31.1_8ede7edd7694646e12d33c52460f622c '@typescript-eslint/eslint-plugin': 4.31.1_8ede7edd7694646e12d33c52460f622c
'@typescript-eslint/parser': 4.31.1_eslint@7.32.0+typescript@4.6.3 '@typescript-eslint/parser': 4.31.1_eslint@7.32.0+typescript@4.6.3
@@ -97,18 +97,18 @@ devDependencies:
eslint-config-prettier: 8.5.0_eslint@7.32.0 eslint-config-prettier: 8.5.0_eslint@7.32.0
eslint-plugin-svelte3: 3.4.1_eslint@7.32.0+svelte@3.47.0 eslint-plugin-svelte3: 3.4.1_eslint@7.32.0+svelte@3.47.0
husky: 7.0.4 husky: 7.0.4
lint-staged: 12.3.7 lint-staged: 12.4.0
postcss: 8.4.12 postcss: 8.4.12
prettier: 2.6.2 prettier: 2.6.2
prettier-plugin-svelte: 2.7.0_prettier@2.6.2+svelte@3.47.0 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 prisma: 3.11.1
svelte: 3.47.0 svelte: 3.47.0
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
svelte-preprocess: 4.10.5_41810887ae6c6d59323116f47e33fa38 svelte-preprocess: 4.10.6_41810887ae6c6d59323116f47e33fa38
svelte-select: 4.4.7 svelte-select: 4.4.7
tailwindcss: 3.0.23_b89136460714832cdda11d1e9d57d1ff tailwindcss: 3.0.24_ts-node@10.7.0
ts-node: 10.7.0_ee885bc7281b682b6adbed6ae09ee090 ts-node: 10.7.0_de7c86b0cde507c63a0402da5b982bd3
tslib: 2.3.1 tslib: 2.3.1
typescript: 4.6.3 typescript: 4.6.3
@@ -374,10 +374,10 @@ packages:
tiny-glob: 0.2.9 tiny-glob: 0.2.9
dev: true dev: true
/@sveltejs/kit/1.0.0-next.310_svelte@3.47.0: /@sveltejs/kit/1.0.0-next.316_svelte@3.47.0:
resolution: resolution:
{ {
integrity: sha512-pTyMyaoyHS+V5cQZIQMfQXmLkhw1VaRwT9avOSgwDc0QBpnNw2LdzwoPYsUr96ca5B6cfT3SMUNolxErTNHmPQ== integrity: sha512-oLjWOWzjriJD2t210r7ALuH/8ZADrJGsOODzRCRSJvRBCt0Q7VKVLqwKbM/RlZzD1k8Af2uRodQT11kP98hAIA==
} }
engines: { node: '>=14.13' } engines: { node: '>=14.13' }
hasBin: true hasBin: true
@@ -468,7 +468,7 @@ packages:
dependencies: dependencies:
'@types/http-cache-semantics': 4.0.1 '@types/http-cache-semantics': 4.0.1
'@types/keyv': 3.1.3 '@types/keyv': 3.1.3
'@types/node': 17.0.23 '@types/node': 17.0.25
'@types/responselike': 1.0.0 '@types/responselike': 1.0.0
dev: false dev: false
@@ -506,7 +506,7 @@ packages:
integrity: sha512-FXCJgyyN3ivVgRoml4h94G/p3kY+u/B86La+QptcqJaWtBWtmc6TtkNfS40n9bIvyLteHh7zXOtgbobORKPbDg== integrity: sha512-FXCJgyyN3ivVgRoml4h94G/p3kY+u/B86La+QptcqJaWtBWtmc6TtkNfS40n9bIvyLteHh7zXOtgbobORKPbDg==
} }
dependencies: dependencies:
'@types/node': 17.0.23 '@types/node': 17.0.25
dev: false dev: false
/@types/node-forge/1.0.1: /@types/node-forge/1.0.1:
@@ -515,22 +515,15 @@ packages:
integrity: sha512-96ELNKv9tQJ19afdBUiM5iDw7OYEc53iUc51gAPR2aGaqRsO1DBROjqgZRjZa1tkPj7TnEOR0EnyAX6iryGkzA== integrity: sha512-96ELNKv9tQJ19afdBUiM5iDw7OYEc53iUc51gAPR2aGaqRsO1DBROjqgZRjZa1tkPj7TnEOR0EnyAX6iryGkzA==
} }
dependencies: dependencies:
'@types/node': 17.0.23 '@types/node': 17.0.25
dev: true dev: true
/@types/node/17.0.23: /@types/node/17.0.25:
resolution: 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: /@types/pug/2.0.5:
resolution: resolution:
{ {
@@ -544,7 +537,7 @@ packages:
integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==
} }
dependencies: dependencies:
'@types/node': 17.0.23 '@types/node': 17.0.25
dev: false dev: false
/@types/sass/1.16.1: /@types/sass/1.16.1:
@@ -553,7 +546,7 @@ packages:
integrity: sha512-iZUcRrGuz/Tbg3loODpW7vrQJkUtpY2fFSf4ELqqkApcS2TkZ1msk7ie8iZPB86lDOP8QOTTmuvWjc5S0R9OjQ== integrity: sha512-iZUcRrGuz/Tbg3loODpW7vrQJkUtpY2fFSf4ELqqkApcS2TkZ1msk7ie8iZPB86lDOP8QOTTmuvWjc5S0R9OjQ==
} }
dependencies: dependencies:
'@types/node': 17.0.23 '@types/node': 17.0.25
dev: true dev: true
/@typescript-eslint/eslint-plugin/4.31.1_8ede7edd7694646e12d33c52460f622c: /@typescript-eslint/eslint-plugin/4.31.1_8ede7edd7694646e12d33c52460f622c:
@@ -1669,16 +1662,16 @@ packages:
ieee754: 1.2.1 ieee754: 1.2.1
dev: false dev: false
/bullmq/1.79.0: /bullmq/1.80.4:
resolution: resolution:
{ {
integrity: sha512-rVtNCDpcWdc+U1MinRtvhJv+GBFNkz0Q3Unf20010qIC6Pj+O2kkIUeepBkCmMNz6G9abrhsee2PheGRJ32+sw== integrity: sha512-j3PyjU16gqmb3Md9QjMInJdbMvxIlSJx7mojtoP06LV9MfhzW75DkDrpSuJlF0H+0+u6MViV4hhaGTxky5OJWw==
} }
dependencies: dependencies:
cron-parser: 4.2.1 cron-parser: 4.2.1
get-port: 5.1.1 get-port: 5.1.1
glob: 7.2.0 glob: 7.2.0
ioredis: 4.28.3 ioredis: 4.28.5
lodash: 4.17.21 lodash: 4.17.21
msgpackr: 1.4.7 msgpackr: 1.4.7
semver: 6.3.0 semver: 6.3.0
@@ -1928,6 +1921,14 @@ packages:
engines: { node: '>= 0.6' } engines: { node: '>= 0.6' }
dev: false dev: false
/cookie/0.5.0:
resolution:
{
integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==
}
engines: { node: '>= 0.6' }
dev: false
/core-js/2.6.12: /core-js/2.6.12:
resolution: resolution:
{ {
@@ -1937,20 +1938,6 @@ packages:
requiresBuild: true requiresBuild: true
dev: 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: /cpu-features/0.0.2:
resolution: resolution:
{ {
@@ -2037,10 +2024,10 @@ packages:
} }
dev: false dev: false
/dayjs/1.11.0: /dayjs/1.11.1:
resolution: resolution:
{ {
integrity: sha512-JLC809s6Y948/FuCZPm5IX8rRhQwOiyMb2TfVVQEixG7P8Lm/gt5S7yoQZmC8x1UehI9Pb7sksEt4xx14m+7Ug== integrity: sha512-ER7EjqVAMkRRsxNCC5YqJ9d9VQYuWdGt7aiH2qA5R5wt8ZmWaP2dLUSIK6y/kVzLMlmh1Tvu5xUf4M/wdGJ5KA==
} }
dev: false dev: false
@@ -2296,15 +2283,6 @@ packages:
ansi-colors: 4.1.1 ansi-colors: 4.1.1
dev: true 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: /es6-promise/3.3.1:
resolution: { integrity: sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM= } resolution: { integrity: sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM= }
dev: true dev: true
@@ -3216,16 +3194,6 @@ packages:
engines: { node: '>= 4' } engines: { node: '>= 4' }
dev: true 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: /import-fresh/3.3.0:
resolution: resolution:
{ {
@@ -3237,16 +3205,6 @@ packages:
resolve-from: 4.0.0 resolve-from: 4.0.0
dev: true 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: /imurmurhash/0.1.4:
resolution: { integrity: sha1-khi5srkoojixPcT7a21XbyMUU+o= } resolution: { integrity: sha1-khi5srkoojixPcT7a21XbyMUU+o= }
engines: { node: '>=0.8.19' } engines: { node: '>=0.8.19' }
@@ -3281,10 +3239,10 @@ packages:
loose-envify: 1.4.0 loose-envify: 1.4.0
dev: true dev: true
/ioredis/4.28.3: /ioredis/4.28.5:
resolution: resolution:
{ {
integrity: sha512-9JOWVgBnuSxpIgfpjc1OeY1OLmA4t2KOWWURTDRXky+eWO0LZhI33pQNT9gYxANUXfh5p/zYephYni6GPRsksQ== integrity: sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==
} }
engines: { node: '>=6' } engines: { node: '>=6' }
dependencies: dependencies:
@@ -3303,10 +3261,6 @@ packages:
- supports-color - supports-color
dev: false dev: false
/is-arrayish/0.2.1:
resolution: { integrity: sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= }
dev: true
/is-binary-path/2.1.0: /is-binary-path/2.1.0:
resolution: resolution:
{ {
@@ -3441,13 +3395,6 @@ packages:
} }
dev: false 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: /json-schema-traverse/0.4.1:
resolution: resolution:
{ {
@@ -3547,14 +3494,18 @@ packages:
engines: { node: '>=10' } engines: { node: '>=10' }
dev: true dev: true
/lines-and-columns/1.1.6: /lilconfig/2.0.5:
resolution: { integrity: sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= }
dev: true
/lint-staged/12.3.7:
resolution: 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 } engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 }
hasBin: true hasBin: true
@@ -3966,10 +3917,10 @@ packages:
path-key: 3.1.1 path-key: 3.1.1
dev: true dev: true
/object-hash/2.2.0: /object-hash/3.0.0:
resolution: resolution:
{ {
integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==
} }
engines: { node: '>= 6' } engines: { node: '>= 6' }
dev: true dev: true
@@ -4067,19 +4018,6 @@ packages:
callsites: 3.1.0 callsites: 3.1.0
dev: true 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: /path-is-absolute/1.0.1:
resolution: { integrity: sha1-F0uSaHNVNP+8es5r9TpanhtcX18= } resolution: { integrity: sha1-F0uSaHNVNP+8es5r9TpanhtcX18= }
engines: { node: '>=0.10.0' } engines: { node: '>=0.10.0' }
@@ -4143,21 +4081,24 @@ packages:
postcss: 8.4.12 postcss: 8.4.12
dev: true 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: resolution:
{ {
integrity: sha512-ipM8Ds01ZUophjDTQYSVP70slFSYg3T0/zyfII5vzhN6V57YSxMgG5syXuwi5VtS8wSf3iL30v0uBdoIVx4Q0g== integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==
} }
engines: { node: '>= 10' } engines: { node: '>= 10' }
peerDependencies: peerDependencies:
postcss: '>=8.0.9'
ts-node: '>=9.0.0' ts-node: '>=9.0.0'
peerDependenciesMeta: peerDependenciesMeta:
postcss:
optional: true
ts-node: ts-node:
optional: true optional: true
dependencies: dependencies:
import-cwd: 3.0.0 lilconfig: 2.0.5
lilconfig: 2.0.4 postcss: 8.4.12
ts-node: 10.7.0_ee885bc7281b682b6adbed6ae09ee090 ts-node: 10.7.0_de7c86b0cde507c63a0402da5b982bd3
yaml: 1.10.2 yaml: 1.10.2
dev: true dev: true
@@ -4171,13 +4112,13 @@ packages:
postcss: ^8.2.14 postcss: ^8.2.14
dependencies: dependencies:
postcss: 8.4.12 postcss: 8.4.12
postcss-selector-parser: 6.0.9 postcss-selector-parser: 6.0.10
dev: true dev: true
/postcss-selector-parser/6.0.9: /postcss-selector-parser/6.0.10:
resolution: resolution:
{ {
integrity: sha512-UO3SgnZOVTwu4kyLR22UQ1xZh086RyNZppb7lLAKBFK8a32ttG5i87Y/P3+2bRSjZNyJ1B7hfFNo273tKe9YxQ== integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==
} }
engines: { node: '>=4' } engines: { node: '>=4' }
dependencies: dependencies:
@@ -4225,10 +4166,10 @@ packages:
svelte: 3.47.0 svelte: 3.47.0
dev: true dev: true
/prettier-plugin-tailwindcss/0.1.8_prettier@2.6.2: /prettier-plugin-tailwindcss/0.1.10_prettier@2.6.2:
resolution: resolution:
{ {
integrity: sha512-hwarSBCswAXa+kqYtaAkFr3Vop9o04WOyZs0qo3NyvW8L7f1rif61wRyq0+ArmVThOuRBcJF5hjGXYk86cwemg== integrity: sha512-ooDGNuXUjgCXfShliVYQ6+0iXqUFXn+zdNInPe0WZN9qINt9srbLGFGY5jeVL4MXtY20/4S8JaBcd8l6N6NfCQ==
} }
engines: { node: '>=12.17.0' } engines: { node: '>=12.17.0' }
peerDependencies: peerDependencies:
@@ -4437,14 +4378,6 @@ packages:
engines: { node: '>=4' } engines: { node: '>=4' }
dev: true dev: true
/resolve-from/5.0.0:
resolution:
{
integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
}
engines: { node: '>=8' }
dev: true
/resolve/1.22.0: /resolve/1.22.0:
resolution: resolution:
{ {
@@ -4903,10 +4836,10 @@ packages:
engines: { node: '>= 0.4' } engines: { node: '>= 0.4' }
dev: true 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: resolution:
{ {
integrity: sha512-POL3IqLUuGqb9DdvuXQaSTNXYnw/odK4hqW86+2LwGcZTdbUPKBBln7pq74wXmcnRE+12bXMY1CvbcUNa2d5nw== integrity: sha512-GrvG24j0+i8AOm0k0KyJ6Dqc+TAR2yzB7rtS4nljHStunVxCTr/1KYlv4EsOeoqtHLzeWMOd5D2O6nDdP/yw4A==
} }
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@@ -4919,7 +4852,7 @@ packages:
sade: 1.7.4 sade: 1.7.4
source-map: 0.7.3 source-map: 0.7.3
svelte: 3.47.0 svelte: 3.47.0
svelte-preprocess: 4.10.5_41810887ae6c6d59323116f47e33fa38 svelte-preprocess: 4.10.6_41810887ae6c6d59323116f47e33fa38
typescript: 4.6.3 typescript: 4.6.3
transitivePeerDependencies: transitivePeerDependencies:
- '@babel/core' - '@babel/core'
@@ -4945,17 +4878,17 @@ packages:
svelte: 3.47.0 svelte: 3.47.0
dev: true dev: true
/svelte-kit-cookie-session/2.1.2: /svelte-kit-cookie-session/2.1.3:
resolution: resolution:
{ {
integrity: sha512-PfxIWDhiyYWu7iKlL0GHpmwDrdFh+rX/WmBzOuvctF25UqngIo9MCiegWBSBLE1RBwNs5UqaIeI8+vligmY07g== integrity: sha512-7Xk3CNbpLAi1KodlsV5W5jULQ2NxQunaXtAYqAuzIEXIq2EwC4oDa25kdmHjNe33epV0t4r0WwxZOuSdJPsapg==
} }
dev: false dev: false
/svelte-preprocess/4.10.5_41810887ae6c6d59323116f47e33fa38: /svelte-preprocess/4.10.6_41810887ae6c6d59323116f47e33fa38:
resolution: resolution:
{ {
integrity: sha512-VKXPRScCzAZqeBZOGq4LLwtNrAu++mVn7XvQox3eFDV7Ciq0Lg70Q8QWjH9iXF7J+pMlXhPsSFwpCb2E+hoeyA== integrity: sha512-I2SV1w/AveMvgIQlUF/ZOO3PYVnhxfcpNyGt8pxpUVhPfyfL/CZBkkw/KPfuFix5FJ9TnnNYMhACK3DtSaYVVQ==
} }
engines: { node: '>= 9.11.2' } engines: { node: '>= 9.11.2' }
requiresBuild: true requiresBuild: true
@@ -5037,7 +4970,7 @@ packages:
strip-ansi: 6.0.1 strip-ansi: 6.0.1
dev: true dev: true
/tailwindcss-scrollbar/0.1.0_tailwindcss@3.0.23: /tailwindcss-scrollbar/0.1.0_tailwindcss@3.0.24:
resolution: resolution:
{ {
integrity: sha512-egipxw4ooQDh94x02XQpPck0P0sfwazwoUGfA9SedPATIuYDR+6qe8d31Gl7YsSMRiOKDkkqfI0kBvEw9lT/Hg== integrity: sha512-egipxw4ooQDh94x02XQpPck0P0sfwazwoUGfA9SedPATIuYDR+6qe8d31Gl7YsSMRiOKDkkqfI0kBvEw9lT/Hg==
@@ -5045,38 +4978,35 @@ packages:
peerDependencies: peerDependencies:
tailwindcss: '>= 2.x.x' tailwindcss: '>= 2.x.x'
dependencies: dependencies:
tailwindcss: 3.0.23_b89136460714832cdda11d1e9d57d1ff tailwindcss: 3.0.24_ts-node@10.7.0
dev: false dev: false
/tailwindcss/3.0.23_b89136460714832cdda11d1e9d57d1ff: /tailwindcss/3.0.24_ts-node@10.7.0:
resolution: resolution:
{ {
integrity: sha512-+OZOV9ubyQ6oI2BXEhzw4HrqvgcARY38xv3zKcjnWtMIZstEsXdI9xftd1iB7+RbOnj2HOEzkA0OyB5BaSxPQA== integrity: sha512-H3uMmZNWzG6aqmg9q07ZIRNIawoiEcNFKDfL+YzOPuPsXuDXxJxB9icqzLgdzKNwjG3SAro2h9SYav8ewXNgig==
} }
engines: { node: '>=12.13.0' } engines: { node: '>=12.13.0' }
hasBin: true hasBin: true
peerDependencies:
autoprefixer: ^10.0.2
dependencies: dependencies:
arg: 5.0.1 arg: 5.0.1
autoprefixer: 10.4.4_postcss@8.4.12
chalk: 4.1.2
chokidar: 3.5.3 chokidar: 3.5.3
color-name: 1.1.4 color-name: 1.1.4
cosmiconfig: 7.0.1
detective: 5.2.0 detective: 5.2.0
didyoumean: 1.2.2 didyoumean: 1.2.2
dlv: 1.1.3 dlv: 1.1.3
fast-glob: 3.2.11 fast-glob: 3.2.11
glob-parent: 6.0.2 glob-parent: 6.0.2
is-glob: 4.0.3 is-glob: 4.0.3
lilconfig: 2.0.5
normalize-path: 3.0.0 normalize-path: 3.0.0
object-hash: 2.2.0 object-hash: 3.0.0
picocolors: 1.0.0
postcss: 8.4.12 postcss: 8.4.12
postcss-js: 4.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-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 postcss-value-parser: 4.2.0
quick-lru: 5.1.1 quick-lru: 5.1.1
resolve: 1.22.0 resolve: 1.22.0
@@ -5148,7 +5078,7 @@ packages:
engines: { node: '>=0.10.0' } engines: { node: '>=0.10.0' }
dev: true dev: true
/ts-node/10.7.0_ee885bc7281b682b6adbed6ae09ee090: /ts-node/10.7.0_de7c86b0cde507c63a0402da5b982bd3:
resolution: resolution:
{ {
integrity: sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A== integrity: sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==
@@ -5170,7 +5100,7 @@ packages:
'@tsconfig/node12': 1.0.9 '@tsconfig/node12': 1.0.9
'@tsconfig/node14': 1.0.1 '@tsconfig/node14': 1.0.1
'@tsconfig/node16': 1.0.2 '@tsconfig/node16': 1.0.2
'@types/node': 17.0.23 '@types/node': 17.0.25
acorn: 8.5.0 acorn: 8.5.0
acorn-walk: 8.2.0 acorn-walk: 8.2.0
arg: 4.1.3 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

@@ -1,6 +1,6 @@
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
binaryTargets = ["linux-musl"] binaryTargets = ["native", "linux-musl"]
} }
datasource db { datasource db {
@@ -91,6 +91,9 @@ model Application {
pythonWSGI String? pythonWSGI String?
pythonModule String? pythonModule String?
pythonVariable String? pythonVariable String?
dockerFileLocation String?
denoMainFile String?
denoOptions String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
settings ApplicationSettings? settings ApplicationSettings?
@@ -126,6 +129,17 @@ model ApplicationPersistentStorage {
@@unique([applicationId, path]) @@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 { model Secret {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
@@ -169,6 +183,7 @@ model Build {
githubAppId String? githubAppId String?
gitlabAppId String? gitlabAppId String?
commit String? commit String?
branch String?
status String? @default("queued") status String? @default("queued")
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@ -267,17 +282,17 @@ model DatabaseSettings {
} }
model Service { model Service {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
fqdn String? fqdn String?
dualCerts Boolean @default(false) dualCerts Boolean @default(false)
type String? type String?
version String? version String?
teams Team[] teams Team[]
destinationDockerId String? destinationDockerId String?
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id]) destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
plausibleAnalytics PlausibleAnalytics? plausibleAnalytics PlausibleAnalytics?
minio Minio? minio Minio?
vscodeserver Vscodeserver? vscodeserver Vscodeserver?
@@ -285,6 +300,7 @@ model Service {
ghost Ghost? ghost Ghost?
serviceSecret ServiceSecret[] serviceSecret ServiceSecret[]
meiliSearch MeiliSearch? meiliSearch MeiliSearch?
persistentStorage ServicePersistentStorage[]
} }
model PlausibleAnalytics { model PlausibleAnalytics {

6
src/app.d.ts vendored
View File

@@ -6,7 +6,11 @@ declare namespace App {
cookies: Record<string, string>; cookies: Record<string, string>;
} }
interface Platform {} interface Platform {}
interface Session extends SessionData {} interface Session extends SessionData {
whiteLabelDetails: {
icon: string | null;
};
}
interface Stuff { interface Stuff {
service: any; service: any;
application: any; application: any;

View File

@@ -8,6 +8,9 @@ import cookie from 'cookie';
import { dev } from '$app/env'; import { dev } from '$app/env';
const whiteLabeled = process.env['COOLIFY_WHITE_LABELED'] === 'true'; const whiteLabeled = process.env['COOLIFY_WHITE_LABELED'] === 'true';
const whiteLabelDetails = {
icon: (whiteLabeled && process.env['COOLIFY_WHITE_LABELED_ICON']) || null
};
export const handle = handleSession( export const handle = handleSession(
{ {
@@ -74,6 +77,7 @@ export const getSession: GetSession = function ({ locals }) {
return { return {
version, version,
whiteLabeled, whiteLabeled,
whiteLabelDetails,
...locals.session.data ...locals.session.data
}; };
}; };

View File

@@ -1,11 +1,9 @@
// TODO: Make this functions generic
async function send({ async function send({
method, method,
path, path,
data = {}, data = {},
headers, headers,
timeout = 30000 timeout = 120000
}): Promise<Record<string, unknown>> { }): Promise<Record<string, unknown>> {
const controller = new AbortController(); const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout); const id = setTimeout(() => controller.abort(), timeout);
@@ -53,7 +51,7 @@ async function send({
export function get( export function get(
path: string, path: string,
headers: Record<string, unknown> headers?: Record<string, unknown>
): Promise<Record<string, unknown>> { ): Promise<Record<string, unknown>> {
return send({ method: 'GET', path, headers }); return send({ method: 'GET', path, headers });
} }
@@ -61,7 +59,7 @@ export function get(
export function del( export function del(
path: string, path: string,
data: Record<string, unknown>, data: Record<string, unknown>,
headers: Record<string, unknown> headers?: Record<string, unknown>
): Promise<Record<string, unknown>> { ): Promise<Record<string, unknown>> {
return send({ method: 'DELETE', path, data, headers }); return send({ method: 'DELETE', path, data, headers });
} }
@@ -69,7 +67,7 @@ export function del(
export function post( export function post(
path: string, path: string,
data: Record<string, unknown>, data: Record<string, unknown>,
headers: Record<string, unknown> headers?: Record<string, unknown>
): Promise<Record<string, unknown>> { ): Promise<Record<string, unknown>> {
return send({ method: 'POST', path, data, headers }); return send({ method: 'POST', path, data, headers });
} }
@@ -77,7 +75,7 @@ export function post(
export function put( export function put(
path: string, path: string,
data: Record<string, unknown>, data: Record<string, unknown>,
headers: Record<string, unknown> headers?: Record<string, unknown>
): Promise<Record<string, unknown>> { ): Promise<Record<string, unknown>> {
return send({ method: 'PUT', path, data, headers }); return send({ method: 'PUT', path, data, headers });
} }

View File

@@ -91,7 +91,9 @@ export const setDefaultConfiguration = async (data) => {
startCommand, startCommand,
buildCommand, buildCommand,
publishDirectory, publishDirectory,
baseDirectory baseDirectory,
dockerFileLocation,
denoMainFile
} = data; } = data;
const template = scanningTemplates[buildPack]; const template = scanningTemplates[buildPack];
if (!port) { if (!port) {
@@ -102,14 +104,25 @@ export const setDefaultConfiguration = async (data) => {
else if (buildPack === 'php') port = 80; else if (buildPack === 'php') port = 80;
else if (buildPack === 'python') port = 8000; else if (buildPack === 'python') port = 8000;
} }
if (!installCommand) installCommand = template?.installCommand || 'yarn install'; if (!installCommand && buildPack !== 'static')
if (!startCommand) startCommand = template?.startCommand || 'yarn start'; installCommand = template?.installCommand || 'yarn install';
if (!buildCommand) buildCommand = template?.buildCommand || null; if (!startCommand && buildPack !== 'static')
startCommand = template?.startCommand || 'yarn start';
if (!buildCommand && buildPack !== 'static') buildCommand = template?.buildCommand || null;
if (!publishDirectory) publishDirectory = template?.publishDirectory || null; if (!publishDirectory) publishDirectory = template?.publishDirectory || null;
if (baseDirectory) { if (baseDirectory) {
if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`; if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`;
if (!baseDirectory.endsWith('/')) baseDirectory = `${baseDirectory}/`; if (!baseDirectory.endsWith('/')) baseDirectory = `${baseDirectory}/`;
} }
if (dockerFileLocation) {
if (!dockerFileLocation.startsWith('/')) dockerFileLocation = `/${dockerFileLocation}`;
if (dockerFileLocation.endsWith('/')) dockerFileLocation = dockerFileLocation.slice(0, -1);
} else {
dockerFileLocation = '/Dockerfile';
}
if (!denoMainFile) {
denoMainFile = 'main.ts';
}
return { return {
buildPack, buildPack,
@@ -118,7 +131,9 @@ export const setDefaultConfiguration = async (data) => {
startCommand, startCommand,
buildCommand, buildCommand,
publishDirectory, 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) { } catch (error) {
console.log(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, buildId,
baseDirectory, baseDirectory,
secrets, secrets,
pullmergeRequestId pullmergeRequestId,
dockerFileLocation
}) { }) {
try { try {
let file = `${workdir}/Dockerfile`; const file = `${workdir}${dockerFileLocation}`;
let dockerFileOut = `${workdir}`;
if (baseDirectory) { if (baseDirectory) {
file = `${workdir}/${baseDirectory}/Dockerfile`; dockerFileOut = `${workdir}${baseDirectory}`;
workdir = `${workdir}/${baseDirectory}`; workdir = `${workdir}${baseDirectory}`;
} }
const Dockerfile: Array<string> = (await fs.readFile(`${file}`, 'utf8')) const Dockerfile: Array<string> = (await fs.readFile(`${file}`, 'utf8'))
.toString() .toString()
.trim() .trim()
@@ -41,8 +42,8 @@ export default async function ({
} }
}); });
} }
await fs.writeFile(`${file}`, Dockerfile.join('\n')); await fs.writeFile(`${dockerFileOut}${dockerFileLocation}`, Dockerfile.join('\n'));
await buildImage({ applicationId, tag, workdir, docker, buildId, debug }); await buildImage({ applicationId, tag, workdir, docker, buildId, debug, dockerFileLocation });
} catch (error) { } catch (error) {
throw error; throw error;
} }

View File

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

View File

@@ -61,14 +61,12 @@ export const saveBuildLog = async ({
buildId: string; buildId: string;
applicationId: string; applicationId: string;
}): Promise<Job> => { }): Promise<Job> => {
if (line) { if (line && typeof line === 'string' && line.includes('ghs_')) {
if (line.includes('ghs_')) { const regex = /ghs_.*@/g;
const regex = /ghs_.*@/g; line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
}
const addTimestamp = `${generateTimestamp()} ${line}`;
return await buildLogQueue.add(buildId, { buildId, line: addTimestamp, applicationId });
} }
const addTimestamp = `${generateTimestamp()} ${line}`;
return await buildLogQueue.add(buildId, { buildId, line: addTimestamp, applicationId });
}; };
export const getTeam = (event: RequestEvent): string | null => { export const getTeam = (event: RequestEvent): string | null => {
@@ -93,11 +91,16 @@ export const getUserDetails = async (
}> => { }> => {
const teamId = getTeam(event); const teamId = getTeam(event);
const userId = event?.locals?.session?.data?.userId || null; const userId = event?.locals?.session?.data?.userId || null;
const { permission = 'read' } = await db.prisma.permission.findFirst({ let permission = 'read';
where: { teamId, userId }, if (teamId && userId) {
select: { permission: true }, const data = await db.prisma.permission.findFirst({
rejectOnNotFound: true where: { teamId, userId },
}); select: { permission: true },
rejectOnNotFound: true
});
if (data.permission) permission = data.permission;
}
const payload = { const payload = {
teamId, teamId,
userId, userId,

View File

@@ -19,7 +19,7 @@ export const staticDeployments = [
'astro', 'astro',
'eleventy' 'eleventy'
]; ];
export const notNodeDeployments = ['php', 'docker', 'rust', 'python']; export const notNodeDeployments = ['php', 'docker', 'rust', 'python', 'deno'];
export function getDomain(domain) { export function getDomain(domain) {
return domain?.replace('https://', '').replace('http://', ''); return domain?.replace('https://', '').replace('http://', '');

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

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

View File

@@ -263,7 +263,10 @@ export async function configureApplication({
publishDirectory, publishDirectory,
pythonWSGI, pythonWSGI,
pythonModule, pythonModule,
pythonVariable pythonVariable,
dockerFileLocation,
denoMainFile,
denoOptions
}: { }: {
id: string; id: string;
buildPack: string; buildPack: string;
@@ -278,6 +281,9 @@ export async function configureApplication({
pythonWSGI: string; pythonWSGI: string;
pythonModule: string; pythonModule: string;
pythonVariable: string; pythonVariable: string;
dockerFileLocation: string;
denoMainFile: string;
denoOptions: string;
}): Promise<Application> { }): Promise<Application> {
return await prisma.application.update({ return await prisma.application.update({
where: { id }, where: { id },
@@ -293,7 +299,10 @@ export async function configureApplication({
publishDirectory, publishDirectory,
pythonWSGI, pythonWSGI,
pythonModule, 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 type { Database, DatabaseSettings } from '@prisma/client';
import generator from 'generate-password'; import generator from 'generate-password';
import forge from 'node-forge'; import forge from 'node-forge';
import getPort, { portNumbers } from 'get-port';
export function generatePassword(length = 24): string { export function generatePassword(length = 24): string {
return generator.generate({ return generator.generate({
@@ -219,7 +220,6 @@ export function generateDatabaseConfiguration(database: Database & { settings: D
return { return {
privatePort: 5432, privatePort: 5432,
environmentVariables: { environmentVariables: {
POSTGRESQL_POSTGRES_PASSWORD: rootUserPassword,
POSTGRESQL_PASSWORD: dbUserPassword, POSTGRESQL_PASSWORD: dbUserPassword,
POSTGRESQL_USERNAME: dbUser, POSTGRESQL_USERNAME: dbUser,
POSTGRESQL_DATABASE: defaultDatabase POSTGRESQL_DATABASE: defaultDatabase
@@ -252,3 +252,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

@@ -18,18 +18,10 @@ export async function listSources(
export async function newSource({ export async function newSource({
name, name,
teamId, teamId
type,
htmlUrl,
apiUrl,
organization
}: { }: {
name: string; name: string;
teamId: string; teamId: string;
type: string;
htmlUrl: string;
apiUrl: string;
organization: string;
}): Promise<GitSource> { }): Promise<GitSource> {
return await prisma.gitSource.create({ return await prisma.gitSource.create({
data: { data: {
@@ -74,8 +66,11 @@ export async function getSource({
if (body?.gitlabApp?.appSecret) body.gitlabApp.appSecret = decrypt(body.gitlabApp.appSecret); if (body?.gitlabApp?.appSecret) body.gitlabApp.appSecret = decrypt(body.gitlabApp.appSecret);
return body; return body;
} }
export async function addGitHubSource({ id, teamId, type, name, htmlUrl, apiUrl }) { export async function addGitHubSource({ id, teamId, type, name, htmlUrl, apiUrl, organization }) {
await prisma.gitSource.update({ where: { id }, data: { type, name, htmlUrl, apiUrl } }); await prisma.gitSource.update({
where: { id },
data: { type, name, htmlUrl, apiUrl, organization }
});
return await prisma.githubApp.create({ return await prisma.githubApp.create({
data: { data: {
teams: { connect: { id: teamId } }, teams: { connect: { id: teamId } },
@@ -95,7 +90,7 @@ export async function addGitLabSource({
appSecret, appSecret,
groupName groupName
}) { }) {
const encrptedAppSecret = encrypt(appSecret); const encryptedAppSecret = encrypt(appSecret);
await prisma.gitSource.update({ where: { id }, data: { type, apiUrl, htmlUrl, name } }); await prisma.gitSource.update({ where: { id }, data: { type, apiUrl, htmlUrl, name } });
return await prisma.gitlabApp.create({ return await prisma.gitlabApp.create({
data: { data: {
@@ -123,10 +118,14 @@ export async function configureGitsource({
} }
export async function updateGitsource({ export async function updateGitsource({
id, id,
name name,
htmlUrl,
apiUrl
}: { }: {
id: string; id: string;
name: string; name: string;
htmlUrl: string;
apiUrl: string;
}): Promise<GitSource> { }): Promise<GitSource> {
return await prisma.gitSource.update({ return await prisma.gitSource.update({
where: { id }, where: { id },

View File

@@ -27,25 +27,35 @@ export async function newService({
export async function getService({ id, teamId }: { id: string; teamId: string }): Promise<Service> { export async function getService({ id, teamId }: { id: string; teamId: string }): Promise<Service> {
let body; let body;
const include = {
destinationDocker: true,
plausibleAnalytics: true,
minio: true,
vscodeserver: true,
wordpress: true,
ghost: true,
serviceSecret: true,
meiliSearch: true
};
if (teamId === '0') { if (teamId === '0') {
body = await prisma.service.findFirst({ body = await prisma.service.findFirst({
where: { id }, where: { id },
include include: {
destinationDocker: true,
plausibleAnalytics: true,
minio: true,
vscodeserver: true,
wordpress: true,
ghost: true,
serviceSecret: true,
meiliSearch: true,
persistentStorage: true
}
}); });
} else { } else {
body = await prisma.service.findFirst({ body = await prisma.service.findFirst({
where: { id, teams: { some: { id: teamId } } }, where: { id, teams: { some: { id: teamId } } },
include include: {
destinationDocker: true,
plausibleAnalytics: true,
minio: true,
vscodeserver: true,
wordpress: true,
ghost: true,
serviceSecret: true,
meiliSearch: true,
persistentStorage: true
}
}); });
} }
@@ -362,6 +372,7 @@ export async function updateGhostService({
} }
export async function removeService({ id }: { id: string }): Promise<void> { 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.meiliSearch.deleteMany({ where: { serviceId: id } });
await prisma.ghost.deleteMany({ where: { serviceId: id } }); await prisma.ghost.deleteMany({ where: { serviceId: id } });
await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } }); await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } });

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
import { dev } from '$app/env'; import { dev } from '$app/env';
import got, { type Got } from 'got'; import got, { type Got } from 'got';
import * as db from '$lib/database'; import * as db from '$lib/database';
import mustache from 'mustache';
import crypto from 'crypto';
import { checkContainer, checkHAProxy } from '.'; import { checkContainer, checkHAProxy } from '.';
import { asyncExecShell, getDomain, getEngine } from '$lib/common'; import { asyncExecShell, getDomain, getEngine } from '$lib/common';
import { supportedServiceTypesAndVersions } from '$lib/components/common'; import { supportedServiceTypesAndVersions } from '$lib/components/common';
@@ -18,10 +20,10 @@ global
defaults defaults
mode http mode http
log global log global
timeout http-request 60s timeout http-request 120s
timeout connect 10s timeout connect 10s
timeout client 60s timeout client 120s
timeout server 60s timeout server 120s
userlist haproxy-dataplaneapi userlist haproxy-dataplaneapi
user admin insecure-password "\${HAPROXY_PASSWORD}" user admin insecure-password "\${HAPROXY_PASSWORD}"
@@ -213,7 +215,8 @@ export async function configureHAProxy(): Promise<void> {
plausibleAnalytics: true, plausibleAnalytics: true,
vscodeserver: true, vscodeserver: true,
wordpress: true, wordpress: true,
ghost: true ghost: true,
meiliSearch: true
} }
}); });
@@ -262,36 +265,20 @@ export async function configureHAProxy(): Promise<void> {
redirectValue, redirectValue,
redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain
}); });
for (const service of services) { }
const { fqdn, id, type, destinationDocker, destinationDockerId, updatedAt } = service; const output = mustache.render(template, data);
if (destinationDockerId) { const newHash = crypto.createHash('md5').update(output).digest('hex');
const { engine } = destinationDocker; const { proxyHash, id } = await db.listSettings();
const found = supportedServiceTypesAndVersions.find((a) => a.name === type); if (proxyHash !== newHash) {
if (found) { await db.prisma.setting.update({ where: { id }, data: { proxyHash: newHash } });
const port = found.ports.main; await haproxy.post(`v2/services/haproxy/configuration/raw`, {
const publicPort = service[type]?.publicPort; searchParams: {
const isRunning = await checkContainer(engine, id); skip_version: true
if (fqdn) { },
const domain = getDomain(fqdn); body: output,
const isHttps = fqdn.startsWith('https://'); headers: {
const isWWW = fqdn.includes('www.'); 'Content-Type': 'text/plain'
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
if (isRunning) {
data.services.push({
id,
port,
publicPort,
domain,
isRunning,
isHttps,
redirectValue,
redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain,
updatedAt: updatedAt.getTime()
});
}
}
}
} }
} });
} }
} }

View File

@@ -126,11 +126,11 @@ export async function startTcpProxy(
const host = getEngine(engine); const host = getEngine(engine);
const containerName = `haproxy-for-${publicPort}`; const containerName = `haproxy-for-${publicPort}`;
const found = await checkContainer(engine, containerName); const found = await checkContainer(engine, containerName, true);
const foundDB = await checkContainer(engine, id); const foundDependentContainer = await checkContainer(engine, id, true);
try { try {
if (foundDB && !found) { if (foundDependentContainer && !found) {
const { stdout: Config } = await asyncExecShell( const { stdout: Config } = await asyncExecShell(
`DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'` `DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'`
); );
@@ -141,6 +141,11 @@ export async function startTcpProxy(
} -d coollabsio/${defaultProxyImageTcp}` } -d coollabsio/${defaultProxyImageTcp}`
); );
} }
if (!foundDependentContainer && found) {
return await asyncExecShell(
`DOCKER_HOST=${host} docker stop -t 0 ${containerName} && docker rm ${containerName}`
);
}
} catch (error) { } catch (error) {
return error; return error;
} }
@@ -156,11 +161,11 @@ export async function startHttpProxy(
const host = getEngine(engine); const host = getEngine(engine);
const containerName = `haproxy-for-${publicPort}`; const containerName = `haproxy-for-${publicPort}`;
const found = await checkContainer(engine, containerName); const found = await checkContainer(engine, containerName, true);
const foundDB = await checkContainer(engine, id); const foundDependentContainer = await checkContainer(engine, id, true);
try { try {
if (foundDB && !found) { if (foundDependentContainer && !found) {
const { stdout: Config } = await asyncExecShell( const { stdout: Config } = await asyncExecShell(
`DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'` `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}` `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) { } catch (error) {
return error; return error;
} }
@@ -176,7 +186,7 @@ export async function startHttpProxy(
export async function startCoolifyProxy(engine: string): Promise<void> { export async function startCoolifyProxy(engine: string): Promise<void> {
const host = getEngine(engine); 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(); const { proxyPassword, proxyUser, id } = await db.listSettings();
if (!found) { if (!found) {
const { stdout: Config } = await asyncExecShell( const { stdout: Config } = await asyncExecShell(
@@ -191,7 +201,25 @@ export async function startCoolifyProxy(engine: string): Promise<void> {
await configureNetworkCoolifyProxy(engine); 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); const host = getEngine(engine);
let containerFound = false; let containerFound = false;
@@ -202,7 +230,10 @@ export async function checkContainer(engine: string, container: string): Promise
const parsedStdout = JSON.parse(stdout); const parsedStdout = JSON.parse(stdout);
const status = parsedStdout.Status; const status = parsedStdout.Status;
const isRunning = status === 'running'; const isRunning = status === 'running';
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}`); await asyncExecShell(`DOCKER_HOST="${host}" docker rm ${container}`);
} }
if (isRunning) { if (isRunning) {
@@ -236,9 +267,15 @@ export async function stopCoolifyProxy(
export async function configureNetworkCoolifyProxy(engine: string): Promise<void> { export async function configureNetworkCoolifyProxy(engine: string): Promise<void> {
const host = getEngine(engine); const host = getEngine(engine);
const destinations = await db.prisma.destinationDocker.findMany({ where: { 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) { for (const destination of destinations) {
await asyncExecShell( if (!configuredNetworks.includes(destination.network)) {
`DOCKER_HOST="${host}" docker network connect ${destination.network} coolify-haproxy` await asyncExecShell(
); `DOCKER_HOST="${host}" docker network connect ${destination.network} coolify-haproxy`
);
}
} }
} }

View File

@@ -6,9 +6,13 @@ import cuid from 'cuid';
import fs from 'fs/promises'; import fs from 'fs/promises';
import getPort, { portNumbers } from 'get-port'; import getPort, { portNumbers } from 'get-port';
import { supportedServiceTypesAndVersions } from '$lib/components/common'; import { supportedServiceTypesAndVersions } from '$lib/components/common';
import { promises as dns } from 'dns';
export async function letsEncrypt(domain: string, id?: string, isCoolify = false): Promise<void> { export async function letsEncrypt(domain: string, id?: string, isCoolify = false): Promise<void> {
try { try {
const certbotImage =
process.arch === 'x64' ? 'certbot/certbot' : 'certbot/certbot:arm64v8-latest';
const data = await db.prisma.setting.findFirst(); const data = await db.prisma.setting.findFirst();
const { minPort, maxPort } = data; const { minPort, maxPort } = data;
@@ -62,7 +66,7 @@ export async function letsEncrypt(domain: string, id?: string, isCoolify = false
if (found) return; if (found) return;
await asyncExecShell( await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:${randomPort} -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port ${randomPort} -d ${nakedDomain} -d ${wwwDomain} --expand --agree-tos --non-interactive --register-unsafely-without-email ${ `DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:${randomPort} -v "coolify-letsencrypt:/etc/letsencrypt" ${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' : '' dev ? '--test-cert' : ''
}` }`
); );
@@ -82,7 +86,7 @@ export async function letsEncrypt(domain: string, id?: string, isCoolify = false
} }
if (found) return; if (found) return;
await asyncExecShell( await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:${randomPort} -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port ${randomPort} -d ${domain} --expand --agree-tos --non-interactive --register-unsafely-without-email ${ `DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:${randomPort} -v "coolify-letsencrypt:/etc/letsencrypt" ${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' : '' dev ? '--test-cert' : ''
}` }`
); );
@@ -148,7 +152,8 @@ export async function generateSSLCerts(): Promise<void> {
plausibleAnalytics: true, plausibleAnalytics: true,
vscodeserver: true, vscodeserver: true,
wordpress: true, wordpress: true,
ghost: true ghost: true,
meiliSearch: true
}, },
orderBy: { createdAt: 'desc' } orderBy: { createdAt: 'desc' }
}); });
@@ -198,16 +203,44 @@ export async function generateSSLCerts(): Promise<void> {
file.endsWith('.pem') && certificates.push(file.replace(/\.pem$/, '')); 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) { for (const ssl of ssls) {
if (!dev) { if (!dev) {
if ( if (
certificates.includes(ssl.domain) || certificates.includes(ssl.domain) ||
certificates.includes(ssl.domain.replace('www.', '')) certificates.includes(ssl.domain.replace('www.', ''))
) { ) {
console.log(`Certificate for ${ssl.domain} already exists`); // console.log(`Certificate for ${ssl.domain} already exists`);
} else { } else {
console.log('Generating SSL for', ssl.domain); // Checking DNS entry before generating certificate
await letsEncrypt(ssl.domain, ssl.id, ssl.isCoolify); 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 { } else {
if ( if (
@@ -216,7 +249,27 @@ export async function generateSSLCerts(): Promise<void> {
) { ) {
console.log(`Certificate for ${ssl.domain} already exists`); console.log(`Certificate for ${ssl.domain} already exists`);
} else { } 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.');
} }
} }
} }

View File

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

View File

@@ -2,8 +2,9 @@ import { asyncExecShell, getEngine, version } from '$lib/common';
import { prisma } from '$lib/database'; import { prisma } from '$lib/database';
export default async function (): Promise<void> { export default async function (): Promise<void> {
const destinationDockers = await prisma.destinationDocker.findMany(); const destinationDockers = await prisma.destinationDocker.findMany();
for (const destinationDocker of destinationDockers) { const engines = [...new Set(destinationDockers.map(({ engine }) => engine))];
const host = getEngine(destinationDocker.engine); for (const engine of engines) {
const host = getEngine(engine);
// Cleanup old coolify images // Cleanup old coolify images
try { try {
let { stdout: images } = await asyncExecShell( let { stdout: images } = await asyncExecShell(
@@ -14,56 +15,23 @@ export default async function (): Promise<void> {
await asyncExecShell(`DOCKER_HOST=${host} docker rmi -f ${images}`); await asyncExecShell(`DOCKER_HOST=${host} docker rmi -f ${images}`);
} }
} catch (error) { } catch (error) {
console.log(error); //console.log(error);
} }
try { try {
await asyncExecShell(`DOCKER_HOST=${host} docker container prune -f`); await asyncExecShell(`DOCKER_HOST=${host} docker container prune -f`);
} catch (error) { } catch (error) {
console.log(error); //console.log(error);
} }
try { try {
await asyncExecShell(`DOCKER_HOST=${host} docker image prune -f --filter "until=2h"`); await asyncExecShell(`DOCKER_HOST=${host} docker image prune -f --filter "until=2h"`);
} catch (error) { } 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

@@ -2,9 +2,12 @@ import * as Bullmq from 'bullmq';
import { default as ProdBullmq, QueueScheduler } from 'bullmq'; import { default as ProdBullmq, QueueScheduler } from 'bullmq';
import { dev } from '$app/env'; import { dev } from '$app/env';
import { prisma } from '$lib/database'; import { prisma } from '$lib/database';
import builder from './builder'; import builder from './builder';
import logger from './logger';
import cleanup from './cleanup'; import cleanup from './cleanup';
import proxy from './proxy'; import proxy from './proxy';
import proxyTcpHttp from './proxyTcpHttp';
import ssl from './ssl'; import ssl from './ssl';
import sslrenewal from './sslrenewal'; import sslrenewal from './sslrenewal';
@@ -27,17 +30,20 @@ const connectionOptions = {
const cron = async (): Promise<void> => { const cron = async (): Promise<void> => {
new QueueScheduler('proxy', connectionOptions); new QueueScheduler('proxy', connectionOptions);
new QueueScheduler('proxyTcpHttp', connectionOptions);
new QueueScheduler('cleanup', connectionOptions); new QueueScheduler('cleanup', connectionOptions);
new QueueScheduler('ssl', connectionOptions); new QueueScheduler('ssl', connectionOptions);
new QueueScheduler('sslRenew', connectionOptions); new QueueScheduler('sslRenew', connectionOptions);
const queue = { const queue = {
proxy: new Queue('proxy', { ...connectionOptions }), proxy: new Queue('proxy', { ...connectionOptions }),
proxyTcpHttp: new Queue('proxyTcpHttp', { ...connectionOptions }),
cleanup: new Queue('cleanup', { ...connectionOptions }), cleanup: new Queue('cleanup', { ...connectionOptions }),
ssl: new Queue('ssl', { ...connectionOptions }), ssl: new Queue('ssl', { ...connectionOptions }),
sslRenew: new Queue('sslRenew', { ...connectionOptions }) sslRenew: new Queue('sslRenew', { ...connectionOptions })
}; };
await queue.proxy.drain(); await queue.proxy.drain();
await queue.proxyTcpHttp.drain();
await queue.cleanup.drain(); await queue.cleanup.drain();
await queue.ssl.drain(); await queue.ssl.drain();
await queue.sslRenew.drain(); await queue.sslRenew.drain();
@@ -52,6 +58,16 @@ const cron = async (): Promise<void> => {
} }
); );
new Worker(
'proxyTcpHttp',
async () => {
await proxyTcpHttp();
},
{
...connectionOptions
}
);
new Worker( new Worker(
'ssl', 'ssl',
async () => { async () => {
@@ -83,6 +99,7 @@ const cron = async (): Promise<void> => {
); );
await queue.proxy.add('proxy', {}, { repeat: { every: 10000 } }); 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 } }); await queue.ssl.add('ssl', {}, { repeat: { every: dev ? 10000 : 60000 } });
if (!dev) await queue.cleanup.add('cleanup', {}, { repeat: { every: 300000 } }); if (!dev) await queue.cleanup.add('cleanup', {}, { repeat: { every: 300000 } });
await queue.sslRenew.add('sslRenew', {}, { repeat: { every: 1800000 } }); await queue.sslRenew.add('sslRenew', {}, { repeat: { every: 1800000 } });
@@ -142,5 +159,9 @@ buildWorker.on('failed', async (job: Bullmq.Job, failedReason) => {
const buildLogQueueName = 'log_queue'; const buildLogQueueName = 'log_queue';
const buildLogQueue = new Queue(buildLogQueueName, connectionOptions); const buildLogQueue = new Queue(buildLogQueueName, connectionOptions);
const buildLogWorker = new Worker(buildLogQueueName, async (job) => await logger(job), {
concurrency: 1,
...connectionOptions
});
export { buildQueue, buildLogQueue }; export { buildQueue, buildLogQueue };

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 = [ export const publicPaths = [
'/login', '/login',
'/register', '/register',
'/reset',
'/reset/password',
'/webhooks/success', '/webhooks/success',
'/webhooks/github', '/webhooks/github',
'/webhooks/github/install', '/webhooks/github/install',

View File

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

View File

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

View File

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

View File

@@ -17,7 +17,7 @@
const endpoint = `/applications/${params.id}.json`; const endpoint = `/applications/${params.id}.json`;
const res = await fetch(endpoint); const res = await fetch(endpoint);
if (res.ok) { 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) { if (!application || Object.entries(application).length === 0) {
return { return {
status: 302, status: 302,
@@ -46,6 +46,7 @@
props: { props: {
application, application,
isRunning, isRunning,
isExited,
githubToken, githubToken,
gitlabToken gitlabToken
}, },
@@ -67,21 +68,32 @@
<script lang="ts"> <script lang="ts">
export let application; export let application;
export let isRunning; export let isRunning;
export let isExited;
export let githubToken; export let githubToken;
export let gitlabToken; export let gitlabToken;
import { page, session } from '$app/stores'; import { page, session } from '$app/stores';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import DeleteIcon from '$lib/components/DeleteIcon.svelte'; import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import Loading from '$lib/components/Loading.svelte'; import Loading from '$lib/components/Loading.svelte';
import { del, post } from '$lib/api'; import { del, get, post } from '$lib/api';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { gitTokens } from '$lib/store'; import { gitTokens } from '$lib/store';
import { toast } from '@zerodevx/svelte-toast'; import { toast } from '@zerodevx/svelte-toast';
import { disabledButton } from '$lib/store';
import { onDestroy, onMount } from 'svelte';
if (githubToken) $gitTokens.githubToken = githubToken; if (githubToken) $gitTokens.githubToken = githubToken;
if (gitlabToken) $gitTokens.gitlabToken = gitlabToken; if (gitlabToken) $gitTokens.gitlabToken = gitlabToken;
let loading = false; let loading = false;
let statusInterval;
$disabledButton =
!$session.isAdmin ||
!application.fqdn ||
!application.gitSource ||
!application.repository ||
!application.destinationDocker ||
!application.buildPack;
const { id } = $page.params; const { id } = $page.params;
async function handleDeploySubmit() { async function handleDeploySubmit() {
@@ -121,142 +133,86 @@
return errorNotification(error); 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> </script>
<nav class="nav-side"> <nav class="nav-side">
{#if loading} {#if loading}
<Loading fullscreen cover /> <Loading fullscreen cover />
{:else} {:else}
{#if application.fqdn && application.gitSource && application.repository && application.destinationDocker && application.buildPack} {#if isExited}
{#if isRunning} <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
? '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 <button
on:click={stopApplication} title="Rebuild application"
title="Stop application"
type="submit" type="submit"
disabled={!$session.isAdmin} disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-red-500" class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 hover:text-green-500"
data-tooltip={$session.isAdmin data-tooltip={$session.isAdmin
? 'Stop application' ? 'Rebuild application'
: 'You do not have permission to stop the 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" />
<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"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -270,24 +226,22 @@
> >
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path <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" /> </svg>
<line x1="12" y1="12" x2="12" y2="14.5" /> </button>
</svg></button </form>
></a {:else}
> <form on:submit|preventDefault={handleDeploySubmit}>
<a
href="/applications/{id}/storage"
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/applications/${id}/storage`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/storage`}
>
<button <button
title="Persistent Storage" title="Build and start application"
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500" type="submit"
data-tooltip="Persistent Storage" 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 <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -300,112 +254,213 @@
stroke-linejoin="round" stroke-linejoin="round"
> >
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<ellipse cx="12" cy="6" rx="8" ry="3" /> <path d="M7 4v16l13 -8z" />
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
</svg> </svg>
</button></a </button>
> </form>
<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" />
{/if} {/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="Application Logs"
disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm"
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={!$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 <button
on:click={() => deleteApplication(application.name)} on:click={() => deleteApplication(application.name)}
title="Delete application" title="Delete application"

View File

@@ -19,13 +19,14 @@
const tempBuildPack = JSON.parse( const tempBuildPack = JSON.parse(
JSON.stringify(findBuildPack(buildPack.name, packageManager)) JSON.stringify(findBuildPack(buildPack.name, packageManager))
); );
delete tempBuildPack.name; delete tempBuildPack.name;
delete tempBuildPack.fancyName; delete tempBuildPack.fancyName;
delete tempBuildPack.color; delete tempBuildPack.color;
delete tempBuildPack.hoverColor; delete tempBuildPack.hoverColor;
if (foundConfig.buildPack !== name) { 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 }); await post(`/applications/${id}/configuration/buildpack.json`, { buildPack: name });
return await goto(from || `/applications/${id}`); return await goto(from || `/applications/${id}`);

View File

@@ -36,8 +36,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 reposSelectOptions;
let branchSelectOptions; let branchSelectOptions;
async function loadRepositories() { async function loadRepositories() {
let page = 1; let page = 1;
let reposCount = 0; let reposCount = 0;
@@ -58,24 +65,28 @@
})); }));
} }
async function loadBranches(event) { async function loadBranches(event) {
branches = [];
selected.repository = event.detail.value; selected.repository = event.detail.value;
loading.branches = true;
selected.branch = undefined;
selected.projectId = repositories.find((repo) => repo.full_name === selected.repository).id; selected.projectId = repositories.find((repo) => repo.full_name === selected.repository).id;
try { let page = 1;
branches = await get(`${apiUrl}/repos/${selected.repository}/branches`, { let branchCount = 0;
Authorization: `token ${$gitTokens.githubToken}` loading.branches = true;
}); const loadedBranches = await loadBranchesByPage();
branchSelectOptions = branches.map((branch) => ({ branches = branches.concat(loadedBranches);
value: branch.name, branchCount = branches.length;
label: branch.name if (branchCount === 100) {
})); while (branchCount === 100) {
return; page = page + 1;
} catch ({ error }) { const nextBranches = await loadBranchesByPage(page);
return errorNotification(error); branches = branches.concat(nextBranches);
} finally { branchCount = nextBranches.length;
loading.branches = false; }
} }
loading.branches = false;
branchSelectOptions = branches.map((branch) => ({
value: branch.name,
label: branch.name
}));
} }
async function isBranchAlreadyUsed(event) { async function isBranchAlreadyUsed(event) {
selected.branch = event.detail.value; selected.branch = event.detail.value;
@@ -166,30 +177,36 @@
{:else} {:else}
<form on:submit|preventDefault={handleSubmit} class="flex flex-col justify-center text-center"> <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-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"> <div class="custom-select-wrapper">
<Select <Select
placeholder={loading.repositories placeholder={loading.repositories
? 'Loading repositories ...' ? 'Loading repositories...'
: 'Please select a repository'} : 'Please select a repository'}
id="repository" id="repository"
showIndicator={true}
isWaiting={loading.repositories}
on:select={loadBranches} on:select={loadBranches}
items={reposSelectOptions} items={reposSelectOptions}
isDisabled={loading.repositories} isDisabled={loading.repositories}
isClearable={false}
/> />
</div> </div>
<input class="hidden" bind:value={selected.projectId} name="projectId" /> <input class="hidden" bind:value={selected.projectId} name="projectId" />
<div class="custom-select-wrapper"> <div class="custom-select-wrapper">
<Select <Select
placeholder={loading.branches placeholder={loading.branches
? 'Loading branches ...' ? 'Loading branches...'
: !selected.repository : !selected.repository
? 'Please select a repository first' ? 'Please select a repository first'
: 'Please select a branch'} : 'Please select a branch'}
id="repository" isWaiting={loading.branches}
showIndicator={selected.repository}
id="branches"
on:select={isBranchAlreadyUsed} on:select={isBranchAlreadyUsed}
items={branchSelectOptions} items={branchSelectOptions}
isDisabled={loading.branches || !selected.repository} isDisabled={loading.branches || !selected.repository}
isClearable={false}
/> />
</div> </div>
</div> </div>
@@ -202,13 +219,6 @@
class:bg-orange-600={showSave} class:bg-orange-600={showSave}
class:hover:bg-orange-500={showSave}>Save</button class:hover:bg-orange-500={showSave}>Save</button
> >
<!-- <button class="w-40"
><a
class="no-underline"
href="{apiUrl}/apps/{application.gitSource.githubApp.name}/installations/new"
>Modify Repositories</a
></button
> -->
</div> </div>
</form> </form>
{/if} {/if}

View File

@@ -31,7 +31,7 @@
import { buildPacks, findBuildPack, scanningTemplates } from '$lib/components/templates'; import { buildPacks, findBuildPack, scanningTemplates } from '$lib/components/templates';
import BuildPack from './_BuildPack.svelte'; import BuildPack from './_BuildPack.svelte';
import { page, session } from '$app/stores'; import { page } from '$app/stores';
import { get } from '$lib/api'; import { get } from '$lib/api';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { gitTokens } from '$lib/store'; import { gitTokens } from '$lib/store';
@@ -221,7 +221,7 @@
<div class="max-w-7xl mx-auto flex flex-wrap justify-center"> <div class="max-w-7xl mx-auto flex flex-wrap justify-center">
{#each buildPacks as buildPack} {#each buildPacks as buildPack}
<div class="p-2"> <div class="p-2">
<BuildPack {buildPack} {scanning} {packageManager} bind:foundConfig /> <BuildPack {buildPack} {scanning} bind:foundConfig />
</div> </div>
{/each} {/each}
</div> </div>

View File

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

View File

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

View File

@@ -48,6 +48,7 @@
import { post } from '$lib/api'; import { post } from '$lib/api';
import cuid from 'cuid'; import cuid from 'cuid';
import { browser } from '$app/env'; import { browser } from '$app/env';
import { disabledButton } from '$lib/store';
const { id } = $page.params; const { id } = $page.params;
let domainEl: HTMLInputElement; let domainEl: HTMLInputElement;
@@ -68,11 +69,6 @@
value: 'Gunicorn', value: 'Gunicorn',
label: 'Gunicorn' label: 'Gunicorn'
} }
// },
// {
// value: 'uWSGI',
// label: 'uWSGI'
// }
]; ];
if (browser && window.location.hostname === 'demo.coolify.io' && !application.fqdn) { if (browser && window.location.hostname === 'demo.coolify.io' && !application.fqdn) {
@@ -127,7 +123,8 @@
try { try {
await post(`/applications/${id}/check.json`, { fqdn: application.fqdn, forceSave }); await post(`/applications/${id}/check.json`, { fqdn: application.fqdn, forceSave });
await post(`/applications/${id}.json`, { ...application }); await post(`/applications/${id}.json`, { ...application });
return window.location.reload(); $disabledButton = false;
return toast.push('Configurations saved.');
} catch ({ error }) { } catch ({ error }) {
if (error?.startsWith('DNS not set')) { if (error?.startsWith('DNS not set')) {
forceSave = true; forceSave = true;
@@ -420,6 +417,48 @@
/> />
</div> </div>
{/if} {/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="grid grid-cols-2 items-center">
<div class="flex-col"> <div class="flex-col">
<label for="baseDirectory" class="pt-2 text-base font-bold text-stone-100" <label for="baseDirectory" class="pt-2 text-base font-bold text-stone-100"

View File

@@ -21,12 +21,11 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; 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 BuildLog from './_BuildLog.svelte';
import { get } from '$lib/api'; import { get } from '$lib/api';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { goto } from '$app/navigation';
export let builds; export let builds;
export let application; export let application;
@@ -174,7 +173,7 @@
> >
<div class="flex-col px-2"> <div class="flex-col px-2">
<div class="text-sm font-bold"> <div class="text-sm font-bold">
{application.branch} {build.branch || application.branch}
</div> </div>
<div class="text-xs"> <div class="text-xs">
{build.type} {build.type}

View File

@@ -22,11 +22,14 @@ export const get: RequestHandler = async (event) => {
if (container) { if (container) {
return { return {
body: { body: {
logs: (await container.logs({ stdout: true, stderr: true, timestamps: true })) logs: (
await container.logs({ stdout: true, stderr: true, timestamps: true, tail: 5000 })
)
.toString() .toString()
.split('\n') .split('\n')
.map((l) => l.slice(8)) .map((l) => l.slice(8))
.filter((a) => a) .filter((a) => a)
.reverse()
} }
}; };
} }

View File

@@ -24,19 +24,23 @@
export let application; export let application;
import { page } from '$app/stores'; import { page } from '$app/stores';
import LoadingLogs from './_Loading.svelte'; import LoadingLogs from './_Loading.svelte';
import { getDomain } from '$lib/components/common';
import { get } from '$lib/api'; import { get } from '$lib/api';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
let loadLogsInterval = null; let loadLogsInterval = null;
let allLogs = {
logs: []
};
let logs = []; let logs = [];
let followingBuild; let currentPage = 1;
let endOfLogs = false;
let startOfLogs = true;
let followingInterval; let followingInterval;
let logsEl; let logsEl;
const { id } = $page.params; const { id } = $page.params;
onMount(async () => { onMount(async () => {
loadLogs(); loadAllLogs();
loadLogsInterval = setInterval(() => { loadLogsInterval = setInterval(() => {
loadLogs(); loadLogs();
}, 1000); }, 1000);
@@ -45,25 +49,55 @@
clearInterval(loadLogsInterval); clearInterval(loadLogsInterval);
clearInterval(followingInterval); clearInterval(followingInterval);
}); });
async function loadLogs() { async function loadAllLogs() {
try { try {
const newLogs = await get(`/applications/${id}/logs.json`); const data: any = await get(`/applications/${id}/logs.json`);
logs = newLogs.logs; allLogs = data.logs;
logs = data.logs.slice(0, 100);
if (logs.length < 100) {
endOfLogs = true;
}
return; return;
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
} }
} }
async function loadLogs() {
function followBuild() { try {
followingBuild = !followingBuild; const newLogs = await get(`/applications/${id}/logs.json`);
if (followingBuild) { logs = newLogs.logs.slice(0, 100);
followingInterval = setInterval(() => { return;
logsEl.scrollTop = logsEl.scrollHeight; } catch ({ error }) {
window.scrollTo(0, document.body.scrollHeight); return errorNotification(error);
}, 100); }
}
async function loadOlderLogs() {
clearInterval(loadLogsInterval);
loadLogsInterval = null;
logsEl.scrollTop = 0;
if (logs.length < 100) {
endOfLogs = true;
return;
}
startOfLogs = false;
endOfLogs = false;
currentPage += 1;
logs = allLogs.slice(currentPage * 100 - 100, currentPage * 100);
}
async function loadNewerLogs() {
currentPage -= 1;
logsEl.scrollTop = 0;
if (currentPage !== 1) {
clearInterval(loadLogsInterval);
endOfLogs = false;
loadLogsInterval = null;
logs = allLogs.slice(currentPage * 100 - 100, currentPage * 100);
} else { } else {
window.clearInterval(followingInterval); startOfLogs = true;
loadLogs();
loadLogsInterval = setInterval(() => {
loadLogs();
}, 1000);
} }
} }
</script> </script>
@@ -145,13 +179,18 @@
<div class="text-xl font-bold tracking-tighter">Waiting for the logs...</div> <div class="text-xl font-bold tracking-tighter">Waiting for the logs...</div>
{:else} {:else}
<div class="relative w-full"> <div class="relative w-full">
<LoadingLogs /> <div class="text-right " />
<div class="flex justify-end sticky top-0 p-2"> {#if loadLogsInterval}
<LoadingLogs />
{/if}
<div class="flex justify-end sticky top-0 p-2 mx-1">
<button <button
on:click={followBuild} on:click={loadOlderLogs}
class="bg-transparent" class:text-coolgray-100={endOfLogs}
data-tooltip="Follow logs" class:hover:bg-coolgray-400={!endOfLogs}
class:text-green-500={followingBuild} class="bg-transparent tooltip-bottom"
data-tooltip="Older logs"
disabled={endOfLogs}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -164,10 +203,33 @@
stroke-linejoin="round" stroke-linejoin="round"
> >
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="12" cy="12" r="9" /> <path
<line x1="8" y1="12" x2="12" y2="16" /> d="M20 15h-8v3.586a1 1 0 0 1 -1.707 .707l-6.586 -6.586a1 1 0 0 1 0 -1.414l6.586 -6.586a1 1 0 0 1 1.707 .707v3.586h8a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1z"
<line x1="12" y1="8" x2="12" y2="16" /> />
<line x1="16" y1="12" x2="12" y2="16" /> </svg>
</button>
<button
on:click={loadNewerLogs}
class:text-coolgray-100={startOfLogs}
class:hover:bg-coolgray-400={!startOfLogs}
class="bg-transparent tooltip-bottom"
data-tooltip="Newer logs"
disabled={startOfLogs}
>
<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="M4 9h8v-3.586a1 1 0 0 1 1.707 -.707l6.586 6.586a1 1 0 0 1 0 1.414l-6.586 6.586a1 1 0 0 1 -1.707 -.707v-3.586h-8a1 1 0 0 1 -1 -1v-4a1 1 0 0 1 1 -1z"
/>
</svg> </svg>
</button> </button>
</div> </div>
@@ -175,7 +237,7 @@
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" 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} bind:this={logsEl}
> >
<div class="px-2"> <div class="px-2 pr-14">
{#each logs as log} {#each logs as log}
{log + '\n'} {log + '\n'}
{/each} {/each}

View File

@@ -19,6 +19,7 @@
import Docker from '$lib/components/svg/applications/Docker.svelte'; import Docker from '$lib/components/svg/applications/Docker.svelte';
import Astro from '$lib/components/svg/applications/Astro.svelte'; import Astro from '$lib/components/svg/applications/Astro.svelte';
import Eleventy from '$lib/components/svg/applications/Eleventy.svelte'; import Eleventy from '$lib/components/svg/applications/Eleventy.svelte';
import Deno from '$lib/components/svg/applications/Deno.svelte';
import { getDomain } from '$lib/components/common'; import { getDomain } from '$lib/components/common';
async function newApplication() { async function newApplication() {
@@ -100,6 +101,8 @@
<Astro /> <Astro />
{:else if application.buildPack.toLowerCase() === 'eleventy'} {:else if application.buildPack.toLowerCase() === 'eleventy'}
<Eleventy /> <Eleventy />
{:else if application.buildPack.toLowerCase() === 'deno'}
<Deno />
{/if} {/if}
{/if} {/if}
@@ -156,6 +159,8 @@
<Astro /> <Astro />
{:else if application.buildPack.toLowerCase() === 'eleventy'} {:else if application.buildPack.toLowerCase() === 'eleventy'}
<Eleventy /> <Eleventy />
{:else if application.buildPack.toLowerCase() === 'deno'}
<Deno />
{/if} {/if}
{/if} {/if}

View File

@@ -29,10 +29,12 @@
disabled={!isRunning} disabled={!isRunning}
readonly={!isRunning} readonly={!isRunning}
placeholder="Generated automatically after start" placeholder="Generated automatically after start"
isPasswordField
id="rootUserPassword" id="rootUserPassword"
name="rootUserPassword" name="rootUserPassword"
bind:value={database.rootUserPassword} bind:value={database.rootUserPassword}
/> />
<Explainer text="Could be changed while the database is running." />
</div> </div>
<div class="grid grid-cols-2 items-center"> <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">User</label>

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,6 @@ import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => { export const get: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
console.log(teamId);
const { id } = event.params; const { id } = event.params;
try { try {
const destination = await db.getDestination({ id, teamId }); const destination = await db.getDestination({ id, teamId });

View File

@@ -13,20 +13,25 @@ export const get: RequestHandler = async (event) => {
select: { id: true, email: true, teams: true } select: { id: true, email: true, teams: true }
}); });
let accounts = []; let accounts = [];
let allTeams = [];
if (teamId === '0') { if (teamId === '0') {
accounts = await db.prisma.user.findMany({ select: { id: true, email: true, teams: true } }); accounts = await db.prisma.user.findMany({ select: { id: true, email: true, teams: true } });
allTeams = await db.prisma.team.findMany({
where: { users: { none: { id: userId } } },
include: { permissions: true }
});
} }
const ownTeams = await db.prisma.team.findMany({
const teams = await db.prisma.permission.findMany({ where: { users: { some: { id: userId } } },
where: { userId: teamId === '0' ? undefined : userId }, include: { permissions: true }
include: { team: { include: { _count: { select: { users: true } } } } }
}); });
const invitations = await db.prisma.teamInvitation.findMany({ where: { uid: userId } }); const invitations = await db.prisma.teamInvitation.findMany({ where: { uid: userId } });
return { return {
status: 200, status: 200,
body: { body: {
teams, ownTeams,
allTeams,
invitations, invitations,
account, account,
accounts accounts

View File

@@ -32,21 +32,12 @@
export let account; export let account;
export let accounts; export let accounts;
export let invitations;
if (accounts.length === 0) { if (accounts.length === 0) {
accounts.push(account); accounts.push(account);
} }
export let teams; export let ownTeams;
export let allTeams;
const ownTeams = teams.filter((team) => {
if (team.team.id === $session.teamId) {
return team;
}
});
const otherTeams = teams.filter((team) => {
if (team.team.id !== $session.teamId) {
return team;
}
});
async function resetPassword(id) { async function resetPassword(id) {
const sure = window.confirm('Are you sure you want to reset the password?'); const sure = window.confirm('Are you sure you want to reset the password?');
@@ -74,12 +65,51 @@
return errorNotification(error); return errorNotification(error);
} }
} }
async function acceptInvitation(id, teamId) {
try {
await post(`/iam/team/${teamId}/invitation/accept.json`, { id });
return window.location.reload();
} catch ({ error }) {
return errorNotification(error);
}
}
async function revokeInvitation(id, teamId) {
try {
await post(`/iam/team/${teamId}/invitation/revoke.json`, { id });
return window.location.reload();
} catch ({ error }) {
return errorNotification(error);
}
}
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Identity and Access Management</div> <div class="mr-4 text-2xl tracking-tight">Identity and Access Management</div>
</div> </div>
{#if invitations.length > 0}
<div class="mx-auto max-w-4xl px-6 py-4">
<div class="title font-bold">Pending invitations</div>
<div class="pt-10 text-center">
{#each invitations as invitation}
<div class="flex justify-center space-x-2">
<div>
Invited to <span class="font-bold text-pink-600">{invitation.teamName}</span> with
<span class="font-bold text-rose-600">{invitation.permission}</span> permission.
</div>
<button
class="hover:bg-green-500"
on:click={() => acceptInvitation(invitation.id, invitation.teamId)}>Accept</button
>
<button
class="hover:bg-red-600"
on:click={() => revokeInvitation(invitation.id, invitation.teamId)}>Delete</button
>
</div>
{/each}
</div>
</div>
{/if}
<div class="mx-auto max-w-4xl px-6 py-4"> <div class="mx-auto max-w-4xl px-6 py-4">
{#if $session.teamId === '0' && accounts.length > 0} {#if $session.teamId === '0' && accounts.length > 0}
<div class="title font-bold">Accounts</div> <div class="title font-bold">Accounts</div>
@@ -127,49 +157,51 @@
<div class="title font-bold">Teams</div> <div class="title font-bold">Teams</div>
<div class="flex items-center justify-center pt-10"> <div class="flex items-center justify-center pt-10">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex flex-col flex-wrap justify-center px-2 pb-10 md:flex-row"> <div class="flex flex-row flex-wrap justify-center px-2 pb-10 md:flex-row">
{#each ownTeams as team} {#each ownTeams as team}
<a href="/iam/team/{team.teamId}" class="w-96 p-2 no-underline"> <a href="/iam/team/{team.id}" class="w-96 p-2 no-underline">
<div <div
class="box-selection relative" class="box-selection relative"
class:hover:bg-cyan-600={team.team?.id !== '0'} class:hover:bg-cyan-600={team.id !== '0'}
class:hover:bg-red-500={team.team?.id === '0'} class:hover:bg-red-500={team.id === '0'}
> >
<div class="truncate text-center text-xl font-bold"> <div class="truncate text-center text-xl font-bold">
{team.team.name} {team.name}
</div> </div>
<div class="truncate text-center font-bold"> <div class="truncate text-center font-bold">
{team.team?.id === '0' ? 'root team' : ''} {team.id === '0' ? 'root team' : ''}
</div> </div>
<div class="mt-1 text-center">{team.team._count.users} member(s)</div> <div class:mt-6={team.id !== '0'} class="mt-1 text-center">
{team.permissions?.length} member(s)
</div>
</div> </div>
</a> </a>
{/each} {/each}
</div> </div>
{#if $session.teamId === '0' && otherTeams.length > 0} {#if $session.teamId === '0' && allTeams.length > 0}
<div class="pb-5 pt-10 text-xl font-bold">Other Teams</div> <div class="pb-5 pt-10 text-xl font-bold">Other Teams</div>
{/if} <div class="flex flex-row flex-wrap justify-center px-2 md:flex-row">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row"> {#each allTeams as team}
{#each otherTeams as team} <a href="/iam/team/{team.id}" class="w-96 p-2 no-underline">
<a href="/iam/team/{team.teamId}" class="w-96 p-2 no-underline"> <div
<div class="box-selection relative"
class="box-selection relative" class:hover:bg-cyan-600={team.id !== '0'}
class:hover:bg-cyan-600={team.team?.id !== '0'} class:hover:bg-red-500={team.id === '0'}
class:hover:bg-red-500={team.team?.id === '0'} >
> <div class="truncate text-center text-xl font-bold">
<div class="truncate text-center text-xl font-bold"> {team.name}
{team.team.name} </div>
</div> <div class="truncate text-center font-bold">
<div class="truncate text-center font-bold"> {team.id === '0' ? 'root team' : ''}
{team.team?.id === '0' ? 'root team' : ''} </div>
</div>
<div class="mt-1 text-center">{team.team._count.users} member(s)</div> <div class="mt-1 text-center">{team.permissions?.length} member(s)</div>
</div> </div>
</a> </a>
{/each} {/each}
</div> </div>
{/if}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -43,8 +43,15 @@
{:else} {:else}
<div class="flex justify-center px-4"> <div class="flex justify-center px-4">
<form on:submit|preventDefault={handleSubmit} class="flex flex-col py-4 space-y-2"> <form on:submit|preventDefault={handleSubmit} class="flex flex-col py-4 space-y-2">
<div class="text-6xl font-bold border-gradient w-48 mx-auto border-b-4">Coolify</div> {#if $session.whiteLabelDetails.icon}
<div class="text-xs text-center font-bold pb-10">v{$session.version}</div> <img
class="w-32 mx-auto pb-8"
src={$session.whiteLabelDetails.icon}
alt="Icon for white labeled version of Coolify"
/>
{:else}
<div class="text-6xl font-bold border-gradient w-48 mx-auto border-b-4 mb-8">Coolify</div>
{/if}
<input <input
type="email" type="email"
name="email" name="email"
@@ -76,10 +83,6 @@
on:click|preventDefault={() => goto('/register')} on:click|preventDefault={() => goto('/register')}
class="bg-transparent hover:bg-coolgray-300 text-white ">Register</button class="bg-transparent hover:bg-coolgray-300 text-white ">Register</button
> >
<button
class="bg-transparent hover:bg-coolgray-300"
on:click|preventDefault={() => goto('/reset')}>Reset password</button
>
</div> </div>
</form> </form>
</div> </div>

18
src/routes/logs.json.ts Normal file
View File

@@ -0,0 +1,18 @@
import type { RequestHandler } from '@sveltejs/kit';
import * as db from '$lib/database';
export const post: RequestHandler = async (event) => {
const data = await event.request.json();
for (const d of data) {
if (d.container_name) {
const { log, container_name: containerId, source } = d;
console.log(log);
// await db.prisma.applicationLogs.create({ data: { log, containerId: containerId.substr(1), source } });
}
}
return {
status: 200,
body: {}
};
};

View File

@@ -11,7 +11,9 @@
let loading = false; let loading = false;
async function handleSubmit() { async function handleSubmit() {
if (loading) return;
try { try {
loading = true;
await post('/new/destination/check.json', { network: payload.network }); await post('/new/destination/check.json', { network: payload.network });
const { id } = await post('/new/destination/docker.json', { const { id } = await post('/new/destination/docker.json', {
...payload ...payload

View File

@@ -64,8 +64,15 @@
{:else} {:else}
<div class="flex justify-center px-4"> <div class="flex justify-center px-4">
<form on:submit|preventDefault={handleSubmit} class="flex flex-col py-4 space-y-2"> <form on:submit|preventDefault={handleSubmit} class="flex flex-col py-4 space-y-2">
<div class="text-6xl font-bold border-gradient w-48 mx-auto border-b-4">Coolify</div> {#if $session.whiteLabelDetails.icon}
<div class="text-xs text-center font-bold pb-10">v{$session.version}</div> <img
class="w-32 mx-auto pb-8"
src={$session.whiteLabelDetails.icon}
alt="Icon for white labeled version of Coolify"
/>
{:else}
<div class="text-6xl font-bold border-gradient w-48 mx-auto border-b-4 mb-8">Coolify</div>
{/if}
<input <input
type="email" type="email"
name="email" name="email"
@@ -105,6 +112,9 @@
{#if userCount === 0} {#if userCount === 0}
<div class="pt-5"> <div class="pt-5">
You are registering the first user. It will be the administrator of your Coolify instance. You are registering the first user. It will be the administrator of your Coolify instance.
<br />
It will take a while, because Coolify will configure itself, the proxy and other docker related
stuff.
</div> </div>
{/if} {/if}
{/if} {/if}

View File

@@ -1,26 +0,0 @@
import type { RequestHandler } from '@sveltejs/kit';
import * as db from '$lib/database';
export const get: RequestHandler = async () => {
const users = await db.prisma.user.findMany({});
return {
status: 200,
body: {
users
}
};
};
export const post: RequestHandler = async (event) => {
const { secretKey } = await event.request.json();
if (secretKey !== process.env.COOLIFY_SECRET_KEY) {
return {
status: 500,
body: {
error: 'Invalid secret key.'
}
};
}
return {
status: 200
};
};

View File

@@ -1,96 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { get, post } from '$lib/api';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast';
let secretKey;
let password = false;
let users = [];
async function handleSubmit() {
try {
await post(`/reset.json`, { secretKey });
password = true;
const data = await get('/reset.json');
users = data.users;
return;
} catch ({ error }) {
return errorNotification(error);
}
}
async function resetPassword(user) {
try {
await post(`/reset/password.json`, { secretKey, user });
toast.push('Password reset done.');
return;
} catch ({ error }) {
return errorNotification(error);
}
}
</script>
<div class="icons fixed top-0 left-0 m-3 cursor-pointer" on:click={() => goto('/')}>
<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" />
<line x1="5" y1="12" x2="19" y2="12" />
<line x1="5" y1="12" x2="11" y2="18" />
<line x1="5" y1="12" x2="11" y2="6" />
</svg>
</div>
<div class="pb-10 pt-24 text-center text-4xl font-bold">Reset Password</div>
<div class="flex items-center justify-center">
{#if password}
<table class="mx-2 text-left">
<thead class="mb-2">
<tr>
<th class="px-2">Email</th>
<th>New password</th>
</tr>
</thead>
<tbody>
{#each users as user}
<tr>
<td class="px-2">{user.email}</td>
<td class="flex space-x-2">
<input
id="newPassword"
name="newPassword"
bind:value={user.newPassword}
placeholder="Super secure new password"
/>
<button
class="mx-auto my-4 w-32 bg-coollabs hover:bg-coollabs-100"
on:click={() => resetPassword(user)}>Reset</button
></td
>
</tr>
{/each}
</tbody>
</table>
{:else}
<form class="flex flex-col" on:submit|preventDefault={handleSubmit}>
<div class="text-center text-2xl py-2 font-bold">Secret Key</div>
<CopyPasswordField
isPasswordField={true}
id="secretKey"
name="secretKey"
bind:value={secretKey}
placeholder="You can find it in ~/coolify/.env (COOLIFY_SECRET_KEY)"
/>
<button type="submit" class="bg-coollabs hover:bg-coollabs-100 mx-auto w-32 my-4"
>Submit</button
>
</form>
{/if}
</div>

View File

@@ -1,27 +0,0 @@
import type { RequestHandler } from '@sveltejs/kit';
import * as db from '$lib/database';
import { ErrorHandler, hashPassword } from '$lib/database';
export const post: RequestHandler = async (event) => {
const { secretKey, user } = await event.request.json();
if (secretKey !== process.env.COOLIFY_SECRET_KEY) {
return {
status: 500,
body: {
error: 'Invalid secret key.'
}
};
}
try {
const hashedPassword = await hashPassword(user.newPassword);
await db.prisma.user.update({
where: { email: user.email },
data: { password: hashedPassword }
});
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
}
};

View File

@@ -62,10 +62,11 @@
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="extraConfig">Extra Config</label> <label for="extraConfig">Extra Config</label>
<textarea <textarea
bind:value={service.wordpress.extraConfig}
disabled={isRunning} disabled={isRunning}
readonly={isRunning} readonly={isRunning}
class:resize-none={isRunning} class:resize-none={isRunning}
rows={isRunning ? 1 : 5} rows="5"
name="extraConfig" name="extraConfig"
id="extraConfig" id="extraConfig"
placeholder={!isRunning placeholder={!isRunning
@@ -74,8 +75,8 @@
define('WP_ALLOW_MULTISITE', true); define('WP_ALLOW_MULTISITE', true);
define('MULTISITE', true); define('MULTISITE', true);
define('SUBDOMAIN_INSTALL', false);` define('SUBDOMAIN_INSTALL', false);`
: 'N/A'}>{service.wordpress.extraConfig}</textarea : 'N/A'}
> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<Setting <Setting

View File

@@ -239,6 +239,35 @@
</svg></button </svg></button
></a ></a
> >
<a
href="/services/{id}/storage"
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/services/${id}/storage`}
class:bg-coolgray-500={$page.url.pathname === `/services/${id}/storage`}
>
<button
title="Persistent Storage"
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
data-tooltip="Persistent Storage"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<ellipse cx="12" cy="6" rx="8" ry="3" />
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
</svg>
</button></a
>
<div class="border border-stone-700 h-8" /> <div class="border border-stone-700 h-8" />
{/if} {/if}
<button <button

View File

@@ -90,7 +90,15 @@ export const post: RequestHandler = async (event) => {
environment: config.ghost.environmentVariables, environment: config.ghost.environmentVariables,
restart: 'always', restart: 'always',
labels: makeLabelForServices('ghost'), labels: makeLabelForServices('ghost'),
depends_on: [`${id}-mariadb`] depends_on: [`${id}-mariadb`],
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
}, },
[`${id}-mariadb`]: { [`${id}-mariadb`]: {
container_name: `${id}-mariadb`, container_name: `${id}-mariadb`,
@@ -98,7 +106,15 @@ export const post: RequestHandler = async (event) => {
networks: [network], networks: [network],
volumes: [config.mariadb.volume], volumes: [config.mariadb.volume],
environment: config.mariadb.environmentVariables, environment: config.mariadb.environmentVariables,
restart: 'always' restart: 'always',
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
} }
}, },
networks: { networks: {
@@ -119,11 +135,7 @@ export const post: RequestHandler = async (event) => {
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try { try {
if (version === 'latest') { await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`
);
}
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return { return {
status: 200 status: 200

View File

@@ -43,7 +43,15 @@ export const post: RequestHandler = async (event) => {
environment: config.environmentVariables, environment: config.environmentVariables,
restart: 'always', restart: 'always',
volumes: [config.volume], volumes: [config.volume],
labels: makeLabelForServices('languagetool') labels: makeLabelForServices('languagetool'),
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
} }
}, },
networks: { networks: {
@@ -61,11 +69,7 @@ export const post: RequestHandler = async (event) => {
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try { try {
if (version === 'latest') { await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`
);
}
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return { return {
status: 200 status: 200

View File

@@ -48,7 +48,15 @@ export const post: RequestHandler = async (event) => {
environment: config.environmentVariables, environment: config.environmentVariables,
restart: 'always', restart: 'always',
volumes: [config.volume], volumes: [config.volume],
labels: makeLabelForServices('meilisearch') labels: makeLabelForServices('meilisearch'),
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
} }
}, },
networks: { networks: {
@@ -66,11 +74,7 @@ export const post: RequestHandler = async (event) => {
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try { try {
if (version === 'latest') { await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`
);
}
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return { return {
status: 200 status: 200

View File

@@ -4,9 +4,7 @@ import { promises as fs } from 'fs';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { startHttpProxy } from '$lib/haproxy'; import { startHttpProxy } from '$lib/haproxy';
import getPort, { portNumbers } from 'get-port'; import { ErrorHandler, getFreePort, getServiceImage } from '$lib/database';
import { getDomain } from '$lib/components/common';
import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile'; import type { ComposeFile } from '$lib/types/composeFile';
@@ -28,13 +26,10 @@ export const post: RequestHandler = async (event) => {
serviceSecret serviceSecret
} = service; } = service;
const data = await db.prisma.setting.findFirst();
const { minPort, maxPort } = data;
const network = destinationDockerId && destinationDocker.network; const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine); const host = getEngine(destinationDocker.engine);
const publicPort = await getPort({ port: portNumbers(minPort, maxPort) }); const publicPort = await getFreePort();
const consolePort = 9001; const consolePort = 9001;
const apiPort = 9000; const apiPort = 9000;
@@ -67,7 +62,15 @@ export const post: RequestHandler = async (event) => {
networks: [network], networks: [network],
volumes: [config.volume], volumes: [config.volume],
restart: 'always', restart: 'always',
labels: makeLabelForServices('minio') labels: makeLabelForServices('minio'),
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
} }
}, },
networks: { networks: {
@@ -85,6 +88,7 @@ export const post: RequestHandler = async (event) => {
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try { try {
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
await db.updateMinioService({ id, publicPort }); await db.updateMinioService({ id, publicPort });
await startHttpProxy(destinationDocker, id, publicPort, apiPort); await startHttpProxy(destinationDocker, id, publicPort, apiPort);

View File

@@ -44,7 +44,15 @@ export const post: RequestHandler = async (event) => {
volumes: [config.volume], volumes: [config.volume],
environment: config.environmentVariables, environment: config.environmentVariables,
restart: 'always', restart: 'always',
labels: makeLabelForServices('n8n') labels: makeLabelForServices('n8n'),
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
} }
}, },
networks: { networks: {
@@ -62,11 +70,7 @@ export const post: RequestHandler = async (event) => {
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try { try {
if (version === 'latest') { await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`
);
}
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return { return {
status: 200 status: 200

View File

@@ -40,7 +40,15 @@ export const post: RequestHandler = async (event) => {
networks: [network], networks: [network],
environment: config.environmentVariables, environment: config.environmentVariables,
restart: 'always', restart: 'always',
labels: makeLabelForServices('nocodb') labels: makeLabelForServices('nocodb'),
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
} }
}, },
networks: { networks: {
@@ -53,11 +61,7 @@ export const post: RequestHandler = async (event) => {
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try { try {
if (version === 'latest') { await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`
);
}
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return { return {
status: 200 status: 200

View File

@@ -133,7 +133,15 @@ COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`;
environment: config.plausibleAnalytics.environmentVariables, environment: config.plausibleAnalytics.environmentVariables,
restart: 'always', restart: 'always',
depends_on: [`${id}-postgresql`, `${id}-clickhouse`], depends_on: [`${id}-postgresql`, `${id}-clickhouse`],
labels: makeLabelForServices('plausibleAnalytics') labels: makeLabelForServices('plausibleAnalytics'),
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '10s',
max_attempts: 5,
window: '120s'
}
}
}, },
[`${id}-postgresql`]: { [`${id}-postgresql`]: {
container_name: `${id}-postgresql`, container_name: `${id}-postgresql`,
@@ -141,7 +149,15 @@ COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`;
networks: [network], networks: [network],
environment: config.postgresql.environmentVariables, environment: config.postgresql.environmentVariables,
volumes: [config.postgresql.volume], volumes: [config.postgresql.volume],
restart: 'always' restart: 'always',
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '10s',
max_attempts: 5,
window: '120s'
}
}
}, },
[`${id}-clickhouse`]: { [`${id}-clickhouse`]: {
build: workdir, build: workdir,
@@ -149,7 +165,15 @@ COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`;
networks: [network], networks: [network],
environment: config.clickhouse.environmentVariables, environment: config.clickhouse.environmentVariables,
volumes: [config.clickhouse.volume], volumes: [config.clickhouse.volume],
restart: 'always' restart: 'always',
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '10s',
max_attempts: 5,
window: '120s'
}
}
} }
}, },
networks: { networks: {
@@ -168,9 +192,7 @@ COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`;
}; };
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
if (version === 'latest') { await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
}
await asyncExecShell( await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up --build -d` `DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up --build -d`
); );

View File

@@ -0,0 +1,73 @@
<script lang="ts">
export let isNew = false;
export let storage = {
id: null,
path: null
};
import { del, post } from '$lib/api';
import { page } from '$app/stores';
import { createEventDispatcher } from 'svelte';
import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast';
const { id } = $page.params;
const dispatch = createEventDispatcher();
async function saveStorage(newStorage = false) {
try {
if (!storage.path) return errorNotification('Path is required.');
storage.path = storage.path.startsWith('/') ? storage.path : `/${storage.path}`;
storage.path = storage.path.endsWith('/') ? storage.path.slice(0, -1) : storage.path;
storage.path.replace(/\/\//g, '/');
await post(`/services/${id}/storage.json`, {
path: storage.path,
storageId: storage.id,
newStorage
});
dispatch('refresh');
if (isNew) {
storage.path = null;
storage.id = null;
}
if (newStorage) toast.push('Storage saved.');
else toast.push('Storage updated.');
} catch ({ error }) {
return errorNotification(error);
}
}
async function removeStorage() {
try {
await del(`/services/${id}/storage.json`, { path: storage.path });
dispatch('refresh');
toast.push('Storage deleted.');
} catch ({ error }) {
return errorNotification(error);
}
}
</script>
<td>
<input
bind:value={storage.path}
required
placeholder="eg: /data"
class=" border border-dashed border-coolgray-300"
/>
</td>
<td>
{#if isNew}
<div class="flex items-center justify-center">
<button class="bg-green-600 hover:bg-green-500" on:click={() => saveStorage(true)}>Add</button
>
</div>
{:else}
<div class="flex flex-row justify-center space-x-2">
<div class="flex items-center justify-center">
<button class="" on:click={() => saveStorage(false)}>Set</button>
</div>
<div class="flex justify-center items-end">
<button class="bg-red-600 hover:bg-red-500" on:click={removeStorage}>Remove</button>
</div>
</div>
{/if}
</td>

View File

@@ -0,0 +1,65 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => {
const { status, body, teamId } = await getUserDetails(event, false);
if (status === 401) return { status, body };
const { id } = event.params;
try {
const persistentStorages = await db.prisma.servicePersistentStorage.findMany({
where: { serviceId: id }
});
return {
body: {
persistentStorages
}
};
} catch (error) {
return ErrorHandler(error);
}
};
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
const { path, newStorage, storageId } = await event.request.json();
try {
if (newStorage) {
await db.prisma.servicePersistentStorage.create({
data: { path, service: { connect: { id } } }
});
} else {
await db.prisma.servicePersistentStorage.update({
where: { id: storageId },
data: { path }
});
}
return {
status: 201
};
} catch (error) {
return ErrorHandler(error);
}
};
export const del: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
const { path } = await event.request.json();
try {
await db.prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id, path } });
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
}
};

View File

@@ -0,0 +1,102 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params, stuff }) => {
let endpoint = `/services/${params.id}/storage.json`;
const res = await fetch(endpoint);
if (res.ok) {
return {
props: {
service: stuff.service,
...(await res.json())
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${endpoint}`)
};
};
</script>
<script lang="ts">
export let service;
export let persistentStorages;
import { page } from '$app/stores';
import Storage from './_Storage.svelte';
import { get } from '$lib/api';
import Explainer from '$lib/components/Explainer.svelte';
import ServiceLinks from '$lib/components/ServiceLinks.svelte';
const { id } = $page.params;
async function refreshStorage() {
const data = await get(`/services/${id}/storage.json`);
persistentStorages = [...data.persistentStorages];
}
</script>
<div
class="flex items-center space-x-2 p-5 px-6 font-bold"
class:p-5={service.fqdn}
class:p-6={!service.fqdn}
>
<div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
Persistent Storage
</div>
<span class="text-xs">{service.name}</span>
</div>
{#if service.fqdn}
<a
href={service.fqdn}
target="_blank"
class="icons tooltip-bottom flex items-center bg-transparent text-sm"
><svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg></a
>
{/if}
<ServiceLinks {service} />
</div>
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4">
<div class="flex justify-center py-4 text-center">
<Explainer
customClass="w-full"
text={'You can specify any folder that you want to be persistent across restarts. <br>This is useful for storing data for VSCode server or WordPress.'}
/>
</div>
<table class="mx-auto border-separate text-left">
<thead>
<tr class="h-12">
<th scope="col">Path</th>
</tr>
</thead>
<tbody>
{#each persistentStorages as storage}
{#key storage.id}
<tr>
<Storage on:refresh={refreshStorage} {storage} />
</tr>
{/key}
{/each}
<tr>
<Storage on:refresh={refreshStorage} isNew />
</tr>
</tbody>
</table>
</div>

View File

@@ -42,7 +42,15 @@ export const post: RequestHandler = async (event) => {
volumes: [config.volume], volumes: [config.volume],
environment: config.environmentVariables, environment: config.environmentVariables,
restart: 'always', restart: 'always',
labels: makeLabelForServices('uptimekuma') labels: makeLabelForServices('uptimekuma'),
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
} }
}, },
networks: { networks: {
@@ -60,11 +68,7 @@ export const post: RequestHandler = async (event) => {
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try { try {
if (version === 'latest') { await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`
);
}
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return { return {
status: 200 status: 200

View File

@@ -43,7 +43,15 @@ export const post: RequestHandler = async (event) => {
networks: [network], networks: [network],
volumes: [config.volume], volumes: [config.volume],
restart: 'always', restart: 'always',
labels: makeLabelForServices('vaultWarden') labels: makeLabelForServices('vaultWarden'),
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
} }
}, },
networks: { networks: {
@@ -60,11 +68,7 @@ export const post: RequestHandler = async (event) => {
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try { try {
if (version === 'latest') { await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`
);
}
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return { return {
status: 200 status: 200

View File

@@ -21,6 +21,7 @@ export const post: RequestHandler = async (event) => {
destinationDockerId, destinationDockerId,
destinationDocker, destinationDocker,
serviceSecret, serviceSecret,
persistentStorage,
vscodeserver: { password } vscodeserver: { password }
} = service; } = service;
@@ -42,6 +43,28 @@ export const post: RequestHandler = async (event) => {
config.environmentVariables[secret.name] = secret.value; config.environmentVariables[secret.name] = secret.value;
}); });
} }
const volumes =
persistentStorage?.map((storage) => {
return `${id}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
}) || [];
const composeVolumes = volumes.map((volume) => {
return {
[`${volume.split(':')[0]}`]: {
name: volume.split(':')[0]
}
};
});
const volumeMounts = Object.assign(
{},
{
[config.volume.split(':')[0]]: {
name: config.volume.split(':')[0]
}
},
...composeVolumes
);
const composeFile: ComposeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
@@ -50,9 +73,17 @@ export const post: RequestHandler = async (event) => {
image: config.image, image: config.image,
environment: config.environmentVariables, environment: config.environmentVariables,
networks: [network], networks: [network],
volumes: [config.volume], volumes: [config.volume, ...volumes],
restart: 'always', restart: 'always',
labels: makeLabelForServices('vscodeServer') labels: makeLabelForServices('vscodeServer'),
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
} }
}, },
networks: { networks: {
@@ -60,19 +91,21 @@ export const post: RequestHandler = async (event) => {
external: true external: true
} }
}, },
volumes: { volumes: volumeMounts
[config.volume.split(':')[0]]: {
name: config.volume.split(':')[0]
}
}
}; };
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
if (version === 'latest') { await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
}
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
const changePermissionOn = persistentStorage.map((p) => p.path);
await asyncExecShell(
`DOCKER_HOST=${host} docker exec -u root ${id} chown -R 1000:1000 ${changePermissionOn.join(
' '
)}`
);
return { return {
status: 200 status: 200
}; };

View File

@@ -2,7 +2,7 @@ import { dev } from '$app/env';
import { asyncExecShell, getEngine, getUserDetails } from '$lib/common'; import { asyncExecShell, getEngine, getUserDetails } from '$lib/common';
import { decrypt, encrypt } from '$lib/crypto'; import { decrypt, encrypt } from '$lib/crypto';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { generateDatabaseConfiguration, ErrorHandler, generatePassword } from '$lib/database'; import { ErrorHandler, generatePassword, getFreePort } from '$lib/database';
import { checkContainer, startTcpProxy, stopTcpHttpProxy } from '$lib/haproxy'; import { checkContainer, startTcpProxy, stopTcpHttpProxy } from '$lib/haproxy';
import type { ComposeFile } from '$lib/types/composeFile'; import type { ComposeFile } from '$lib/types/composeFile';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
@@ -16,11 +16,10 @@ export const post: RequestHandler = async (event) => {
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { id } = event.params; const { id } = event.params;
const data = await db.prisma.setting.findFirst();
const { minPort, maxPort } = data;
const { ftpEnabled } = await event.request.json(); const { ftpEnabled } = await event.request.json();
const publicPort = await getPort({ port: portNumbers(minPort, maxPort) }); const publicPort = await getFreePort();
let ftpUser = cuid(); let ftpUser = cuid();
let ftpPassword = generatePassword(); let ftpPassword = generatePassword();
@@ -114,7 +113,7 @@ export const post: RequestHandler = async (event) => {
services: { services: {
[`${id}-ftp`]: { [`${id}-ftp`]: {
image: `atmoz/sftp:alpine`, image: `atmoz/sftp:alpine`,
command: `'${ftpUser}:${password.replace('\n', '').replace(/\$/g, '$$$')}:e:1001'`, command: `'${ftpUser}:${password.replace('\n', '').replace(/\$/g, '$$$')}:e:33'`,
extra_hosts: ['host.docker.internal:host-gateway'], extra_hosts: ['host.docker.internal:host-gateway'],
container_name: `${id}-ftp`, container_name: `${id}-ftp`,
volumes, volumes,

View File

@@ -77,7 +77,15 @@ export const post: RequestHandler = async (event) => {
networks: [network], networks: [network],
restart: 'always', restart: 'always',
depends_on: [`${id}-mysql`], depends_on: [`${id}-mysql`],
labels: makeLabelForServices('wordpress') labels: makeLabelForServices('wordpress'),
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
}, },
[`${id}-mysql`]: { [`${id}-mysql`]: {
container_name: `${id}-mysql`, container_name: `${id}-mysql`,
@@ -85,7 +93,15 @@ export const post: RequestHandler = async (event) => {
volumes: [config.mysql.volume], volumes: [config.mysql.volume],
environment: config.mysql.environmentVariables, environment: config.mysql.environmentVariables,
networks: [network], networks: [network],
restart: 'always' restart: 'always',
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
} }
}, },
networks: { networks: {
@@ -105,11 +121,7 @@ export const post: RequestHandler = async (event) => {
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try { try {
if (version === 'latest') { await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`
);
}
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return { return {
status: 200 status: 200

View File

@@ -12,21 +12,44 @@ export const post: RequestHandler = async (event) => {
try { try {
const service = await db.getService({ id, teamId }); const service = await db.getService({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service; const {
destinationDockerId,
destinationDocker,
fqdn,
wordpress: { ftpEnabled }
} = service;
if (destinationDockerId) { if (destinationDockerId) {
const engine = destinationDocker.engine; const engine = destinationDocker.engine;
try { try {
let found = await checkContainer(engine, id); const found = await checkContainer(engine, id);
if (found) { if (found) {
await removeDestinationDocker({ id, engine }); await removeDestinationDocker({ id, engine });
} }
found = await checkContainer(engine, `${id}-mysql`); } catch (error) {
console.error(error);
}
try {
const found = await checkContainer(engine, `${id}-mysql`);
if (found) { if (found) {
await removeDestinationDocker({ id: `${id}-mysql`, engine }); await removeDestinationDocker({ id: `${id}-mysql`, engine });
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
try {
if (ftpEnabled) {
const found = await checkContainer(engine, `${id}-ftp`);
if (found) {
await removeDestinationDocker({ id: `${id}-ftp`, engine });
}
await db.prisma.wordpress.update({
where: { serviceId: id },
data: { ftpEnabled: false }
});
}
} catch (error) {
console.error(error);
}
} }
return { return {

View File

@@ -2,6 +2,7 @@
export let source; export let source;
import { page, session } from '$app/stores'; import { page, session } from '$app/stores';
import { post } from '$lib/api'; import { post } from '$lib/api';
import Explainer from '$lib/components/Explainer.svelte';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast'; import { toast } from '@zerodevx/svelte-toast';
const { id } = $page.params; const { id } = $page.params;
@@ -51,7 +52,8 @@
type: 'github', type: 'github',
name: source.name, name: source.name,
htmlUrl: source.htmlUrl.replace(/\/$/, ''), htmlUrl: source.htmlUrl.replace(/\/$/, ''),
apiUrl: source.apiUrl.replace(/\/$/, '') apiUrl: source.apiUrl.replace(/\/$/, ''),
organization: source.organization
}); });
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
@@ -97,6 +99,22 @@
<label for="apiUrl" class="text-base font-bold text-stone-100">API URL</label> <label for="apiUrl" class="text-base font-bold text-stone-100">API URL</label>
<input name="apiUrl" id="apiUrl" required bind:value={source.apiUrl} /> <input name="apiUrl" id="apiUrl" required bind:value={source.apiUrl} />
</div> </div>
<div class="grid grid-cols-2">
<div class="flex flex-col">
<label for="organization" class="pt-2 text-base font-bold text-stone-100"
>Organization</label
>
<Explainer
text="Fill it if you would like to use an organization's as your Git Source. Otherwise your user will be used."
/>
</div>
<input
name="organization"
id="organization"
placeholder="eg: coollabsio"
bind:value={source.organization}
/>
</div>
</div> </div>
{#if source.apiUrl && source.htmlUrl && source.name} {#if source.apiUrl && source.htmlUrl && source.name}
<div class="text-center"> <div class="text-center">
@@ -135,6 +153,21 @@
<label for="apiUrl" class="text-base font-bold text-stone-100">API URL</label> <label for="apiUrl" class="text-base font-bold text-stone-100">API URL</label>
<input name="apiUrl" id="apiUrl" required bind:value={source.apiUrl} /> <input name="apiUrl" id="apiUrl" required bind:value={source.apiUrl} />
</div> </div>
<div class="grid grid-cols-2">
<div class="flex flex-col">
<label for="organization" class="pt-2 text-base font-bold text-stone-100"
>Organization</label
>
</div>
<input
readonly
disabled
name="organization"
id="organization"
placeholder="eg: coollabsio"
bind:value={source.organization}
/>
</div>
</div> </div>
</form> </form>
{:else} {:else}

View File

@@ -9,8 +9,8 @@ export const post: RequestHandler = async (event) => {
const { id } = event.params; const { id } = event.params;
try { try {
let { type, name, htmlUrl, apiUrl } = await event.request.json(); let { type, name, htmlUrl, apiUrl, organization } = await event.request.json();
await db.addGitHubSource({ id, teamId, type, name, htmlUrl, apiUrl }); await db.addGitHubSource({ id, teamId, type, name, htmlUrl, apiUrl, organization });
return { status: 201 }; return { status: 201 };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);

View File

@@ -36,7 +36,6 @@
export let settings; export let settings;
onMount(() => { onMount(() => {
const { organization, id, htmlUrl } = source; const { organization, id, htmlUrl } = source;
console.log(source);
const { fqdn } = settings; const { fqdn } = settings;
const host = dev const host = dev
? 'http://localhost:3000' ? 'http://localhost:3000'

View File

@@ -50,7 +50,10 @@ textarea {
} }
#svelte .listContainer { #svelte .listContainer {
@apply bg-coolgray-400 text-white scrollbar-w-2 scrollbar-thumb-coollabs scrollbar-track-coolgray-200; @apply bg-coolgray-400 text-white scrollbar-w-2 scrollbar-thumb-green-500 scrollbar-track-coolgray-200;
}
#svelte .selectedItem {
@apply pl-3;
} }
#svelte .item.hover { #svelte .item.hover {
@@ -116,7 +119,7 @@ a {
} }
.icons { .icons {
@apply rounded p-2 transition duration-200 hover:bg-coolgray-500 disabled:bg-coolblack !important; @apply rounded p-2 transition duration-200 hover:bg-coolgray-500 disabled:bg-coolblack disabled:text-coolgray-500 !important;
} }
.arrow-right-applications { .arrow-right-applications {