Merge pull request #656 from coollabsio/next

v3.10.15
This commit is contained in:
Andras Bacsai 2022-10-12 14:51:56 +02:00 committed by GitHub
commit fb955e15f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 2321 additions and 1242 deletions

View File

@ -5,7 +5,7 @@ on:
types: [released] types: [released]
jobs: jobs:
arm64-build: arm64:
runs-on: [self-hosted, arm64] runs-on: [self-hosted, arm64]
steps: steps:
- name: Checkout - name: Checkout
@ -31,7 +31,7 @@ jobs:
tags: coollabsio/coolify:${{steps.package-version.outputs.current-version}}-arm64 tags: coollabsio/coolify:${{steps.package-version.outputs.current-version}}-arm64
cache-from: type=registry,ref=coollabsio/coolify:buildcache-arm64 cache-from: type=registry,ref=coollabsio/coolify:buildcache-arm64
cache-to: type=registry,ref=coollabsio/coolify:buildcache-arm64,mode=max cache-to: type=registry,ref=coollabsio/coolify:buildcache-arm64,mode=max
amd64-build: amd64:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@ -57,9 +57,35 @@ jobs:
tags: coollabsio/coolify:${{steps.package-version.outputs.current-version}}-amd64 tags: coollabsio/coolify:${{steps.package-version.outputs.current-version}}-amd64
cache-from: type=registry,ref=coollabsio/coolify:buildcache-amd64 cache-from: type=registry,ref=coollabsio/coolify:buildcache-amd64
cache-to: type=registry,ref=coollabsio/coolify:buildcache-amd64,mode=max cache-to: type=registry,ref=coollabsio/coolify:buildcache-amd64,mode=max
aarch64:
runs-on: [self-hosted, arm64]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Get current package version
uses: martinbeentjes/npm-get-version-action@v1.2.3
id: package-version
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/aarch64
push: true
tags: coollabsio/coolify:${{steps.package-version.outputs.current-version}}-aarch64
cache-from: type=registry,ref=coollabsio/coolify:buildcache-aarch64
cache-to: type=registry,ref=coollabsio/coolify:buildcache-aarch64,mode=max
merge-manifest: merge-manifest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [amd64-build, arm64-build] needs: [amd64, arm64, aarch64]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
@ -77,7 +103,7 @@ jobs:
id: package-version id: package-version
- name: Create & publish manifest - name: Create & publish manifest
run: | run: |
docker manifest create coollabsio/coolify:${{steps.package-version.outputs.current-version}} --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-amd64 --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-arm64 docker manifest create coollabsio/coolify:${{steps.package-version.outputs.current-version}} --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-amd64 --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-arm64 --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-aarch64
docker manifest push coollabsio/coolify:${{steps.package-version.outputs.current-version}} docker manifest push coollabsio/coolify:${{steps.package-version.outputs.current-version}}
- uses: sarisia/actions-status-discord@v1 - uses: sarisia/actions-status-discord@v1
if: always() if: always()

View File

@ -6,7 +6,7 @@ on:
- next - next
jobs: jobs:
arm64-making-something-cool: arm64:
runs-on: [self-hosted, arm64] runs-on: [self-hosted, arm64]
steps: steps:
- name: Checkout - name: Checkout
@ -34,7 +34,7 @@ jobs:
tags: coollabsio/coolify:next-arm64 tags: coollabsio/coolify:next-arm64
cache-from: type=registry,ref=coollabsio/coolify:buildcache-next-arm64 cache-from: type=registry,ref=coollabsio/coolify:buildcache-next-arm64
cache-to: type=registry,ref=coollabsio/coolify:buildcache-next-arm64,mode=max cache-to: type=registry,ref=coollabsio/coolify:buildcache-next-arm64,mode=max
amd64-making-something-cool: amd64:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@ -59,12 +59,12 @@ jobs:
context: . context: .
platforms: linux/amd64 platforms: linux/amd64
push: true push: true
tags: coollabsio/coolify:next-amd64,coollabsio/coolify:next-test tags: coollabsio/coolify:next-amd64
cache-from: type=registry,ref=coollabsio/coolify:buildcache-next-amd64 cache-from: type=registry,ref=coollabsio/coolify:buildcache-next-amd64
cache-to: type=registry,ref=coollabsio/coolify:buildcache-next-amd64,mode=max cache-to: type=registry,ref=coollabsio/coolify:buildcache-next-amd64,mode=max
merge-manifest-to-be-cool: merge-manifest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [arm64-making-something-cool, amd64-making-something-cool] needs: [arm64, amd64]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3

View File

@ -5,19 +5,27 @@ # Contribution
You can ask for guidance anytime on our Discord server in the #contribution channel. You can ask for guidance anytime on our Discord server in the #contribution channel.
## Setup your development environment ## Setup your development environment
### Container based development flow (recommended and the easiest)
All you need is to intall [Docker Engine 20.11+](https://docs.docker.com/engine/install/) on your local machine and run `pnpm dev:container`. It will build the base image for Coolify and start the development server inside Docker. All required ports (3000, 3001) will be exposed to your host.
### Github codespaces ### Github codespaces
If you have github codespaces enabled then you can just create a codespace and run `pnpm dev` to run your the dev environment. All the required dependencies and packages has been configured for you already. If you have github codespaces enabled then you can just create a codespace and run `pnpm dev` to run your the dev environment. All the required dependencies and packages has been configured for you already.
### Gitpod ### Gitpod
1. Use [container based development flow](#container-based-development-flow-easiest)
2. Or setup your workspace manually:
If you have a [Gitpod](https://gitpod.io), you can just create a workspace from this repository, run `pnpm install && pnpm db:push && pnpm db:seed` and then `pnpm dev`. All the required dependencies and packages has been configured for you already. Create a workspace from this repository, run `pnpm install && pnpm db:push && pnpm db:seed` and then `pnpm dev`. All the required dependencies and packages has been configured for you already.
> Some packages, just `pack` are not installed in this way. You cannot test all the features. Please use the [container based development flow](#container-based-development-flow-easiest).
### Local Machine ### Local Machine
> At the moment, Coolify `doesn't support Windows`. You must use `Linux` or `MacOS` or consider using Gitpod or Github Codespaces. > At the moment, Coolify `doesn't support Windows`. You must use `Linux` or `MacOS` or consider using Gitpod or Github Codespaces.
- 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! Install all the prerequisites manually to your host system. If you would not like to install anything, I suggest to use the [container based development flow](#container-based-development-flow-easiest).
- 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.
- You need to have [Docker Compose Plugin](https://docs.docker.com/compose/install/compose-plugin/) installed locally. - You need to have [Docker Compose Plugin](https://docs.docker.com/compose/install/compose-plugin/) installed locally.
- You need to have [GIT LFS Support](https://git-lfs.github.com/) installed locally. - You need to have [GIT LFS Support](https://git-lfs.github.com/) installed locally.

View File

@ -1,5 +1,4 @@
ARG PNPM_VERSION=7.11.0 ARG PNPM_VERSION=7.11.0
ARG NPM_VERSION=8.19.1
FROM node:18-slim as build FROM node:18-slim as build
WORKDIR /app WORKDIR /app
@ -17,20 +16,26 @@ WORKDIR /app
ENV NODE_ENV production ENV NODE_ENV production
ARG TARGETPLATFORM ARG TARGETPLATFORM
# https://download.docker.com/linux/static/stable/
ARG DOCKER_VERSION=20.10.18
# https://github.com/docker/compose/releases
# Reverted to 2.6.1 because of this https://github.com/docker/compose/issues/9704. 2.9.0 still has a bug.
ARG DOCKER_COMPOSE_VERSION=2.6.1
# https://github.com/buildpacks/pack/releases
ARG PACK_VERSION=v0.27.0
RUN apt update && apt -y install --no-install-recommends ca-certificates git git-lfs openssh-client curl jq cmake sqlite3 openssl psmisc python3 RUN apt update && apt -y install --no-install-recommends ca-certificates git git-lfs openssh-client curl jq cmake sqlite3 openssl psmisc python3
RUN apt-get clean autoclean && apt-get autoremove --yes && rm -rf /var/lib/{apt,dpkg,cache,log}/ RUN apt-get clean autoclean && apt-get autoremove --yes && rm -rf /var/lib/{apt,dpkg,cache,log}/
RUN npm --no-update-notifier --no-fund --global install pnpm@${PNPM_VERSION} RUN npm --no-update-notifier --no-fund --global install pnpm@${PNPM_VERSION}
RUN npm install -g npm@${PNPM_VERSION} RUN npm install -g npm@${PNPM_VERSION}
RUN mkdir -p ~/.docker/cli-plugins/ RUN mkdir -p ~/.docker/cli-plugins/
# https://download.docker.com/linux/static/stable/
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-20.10.9 -o /usr/bin/docker
# https://github.com/docker/compose/releases
# Reverted to 2.6.1 because of this https://github.com/docker/compose/issues/9704. 2.9.0 still has a bug.
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-compose-linux-2.6.1 -o ~/.docker/cli-plugins/docker-compose
RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker
RUN (curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.27.0/pack-v0.27.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack) RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-$DOCKER_VERSION -o /usr/bin/docker
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-compose-linux-$DOCKER_COMPOSE_VERSION -o ~/.docker/cli-plugins/docker-compose
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/pack-$PACK_VERSION -o /usr/local/bin/pack
RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker /usr/local/bin/pack
COPY --from=build /app/apps/api/build/ . COPY --from=build /app/apps/api/build/ .
COPY --from=build /app/others/fluentbit/ ./fluentbit COPY --from=build /app/others/fluentbit/ ./fluentbit

31
Dockerfile-dev Normal file
View File

@ -0,0 +1,31 @@
FROM node:18-slim
ENV NODE_ENV development
ARG TARGETPLATFORM
ARG PNPM_VERSION=7.11.0
ARG NPM_VERSION=8.19.1
# https://download.docker.com/linux/static/stable/
ARG DOCKER_VERSION=20.10.18
# https://github.com/docker/compose/releases
# Reverted to 2.6.1 because of this https://github.com/docker/compose/issues/9704. 2.9.0 still has a bug.
ARG DOCKER_COMPOSE_VERSION=2.6.1
# https://github.com/buildpacks/pack/releases
ARG PACK_VERSION=v0.27.0
WORKDIR /app
RUN npm --no-update-notifier --no-fund --global install pnpm@${PNPM_VERSION}
RUN apt update && apt -y install --no-install-recommends ca-certificates git git-lfs openssh-client curl jq cmake sqlite3 openssl psmisc python3
RUN apt-get clean autoclean && apt-get autoremove --yes && rm -rf /var/lib/{apt,dpkg,cache,log}/
RUN npm --no-update-notifier --no-fund --global install pnpm@${PNPM_VERSION}
RUN npm install -g npm@${PNPM_VERSION}
RUN mkdir -p ~/.docker/cli-plugins/
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-$DOCKER_VERSION -o /usr/bin/docker
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-compose-linux-$DOCKER_COMPOSE_VERSION -o ~/.docker/cli-plugins/docker-compose
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/pack-$PACK_VERSION -o /usr/local/bin/pack
RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker /usr/local/bin/pack
EXPOSE 3000
ENV CHECKPOINT_DISABLE=1

View File

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

View File

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

View File

@ -117,6 +117,9 @@ model Application {
dockerFileLocation String? dockerFileLocation String?
denoMainFile String? denoMainFile String?
denoOptions String? denoOptions String?
dockerComposeFile String?
dockerComposeFileLocation String?
dockerComposeConfiguration String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
destinationDockerId String? destinationDockerId String?

View File

@ -6,11 +6,14 @@ import cookie from '@fastify/cookie';
import multipart from '@fastify/multipart'; import multipart from '@fastify/multipart';
import path, { join } from 'path'; import path, { join } from 'path';
import autoLoad from '@fastify/autoload'; import autoLoad from '@fastify/autoload';
import { asyncExecShell, createRemoteEngineConfiguration, getDomain, isDev, listSettings, prisma, version } from './lib/common'; import { asyncExecShell, cleanupDockerStorage, createRemoteEngineConfiguration, decrypt, executeDockerCmd, executeSSHCmd, generateDatabaseConfiguration, getDomain, isDev, listSettings, prisma, startTraefikProxy, startTraefikTCPProxy, version } from './lib/common';
import { scheduler } from './lib/scheduler'; import { scheduler } from './lib/scheduler';
import { compareVersions } from 'compare-versions'; import { compareVersions } from 'compare-versions';
import Graceful from '@ladjs/graceful' import Graceful from '@ladjs/graceful'
import axios from 'axios';
import fs from 'fs/promises';
import { verifyRemoteDockerEngineFn } from './routes/api/v1/destinations/handlers'; import { verifyRemoteDockerEngineFn } from './routes/api/v1/destinations/handlers';
import { checkContainer } from './lib/docker';
declare module 'fastify' { declare module 'fastify' {
interface FastifyInstance { interface FastifyInstance {
config: { config: {
@ -72,7 +75,6 @@ const host = '0.0.0.0';
} }
}; };
const options = { const options = {
schema, schema,
dotenv: true dotenv: true
@ -131,29 +133,26 @@ const host = '0.0.0.0';
if (!scheduler.workers.has('deployApplication')) { if (!scheduler.workers.has('deployApplication')) {
scheduler.run('deployApplication'); scheduler.run('deployApplication');
} }
if (!scheduler.workers.has('infrastructure')) {
scheduler.run('infrastructure');
}
}, 2000) }, 2000)
// autoUpdater // autoUpdater
setInterval(async () => { setInterval(async () => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:autoUpdater") await autoUpdater()
}, 60000 * 15) }, 60000 * 15)
// cleanupStorage // cleanupStorage
setInterval(async () => { setInterval(async () => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupStorage") await cleanupStorage()
}, 60000 * 10) }, 60000 * 10)
// checkProxies and checkFluentBit // checkProxies and checkFluentBit
setInterval(async () => { setInterval(async () => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:checkProxies") await checkProxies();
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:checkFluentBit") await checkFluentBit();
}, 10000) }, 10000)
setInterval(async () => { setInterval(async () => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:copySSLCertificates") await copySSLCertificates();
}, 2000) }, 2000)
await Promise.all([ await Promise.all([
@ -165,9 +164,6 @@ const host = '0.0.0.0';
console.error(error); console.error(error);
process.exit(1); process.exit(1);
} }
})(); })();
@ -227,3 +223,237 @@ async function configureRemoteDockers() {
console.log(error) console.log(error)
} }
} }
async function autoUpdater() {
try {
const currentVersion = version;
const { data: versions } = await axios
.get(
`https://get.coollabs.io/versions.json`
, {
params: {
appId: process.env['COOLIFY_APP_ID'] || undefined,
version: currentVersion
}
})
const latestVersion = versions['coolify'].main.version;
const isUpdateAvailable = compareVersions(latestVersion, currentVersion);
if (isUpdateAvailable === 1) {
const activeCount = 0
if (activeCount === 0) {
if (!isDev) {
const { isAutoUpdateEnabled } = await prisma.setting.findFirst();
if (isAutoUpdateEnabled) {
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
await asyncExecShell(`env | grep '^COOLIFY' > .env`);
await asyncExecShell(
`sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env`
);
await asyncExecShell(
`docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"`
);
}
} else {
console.log('Updating (not really in dev mode).');
}
}
}
} catch (error) { }
}
async function checkFluentBit() {
try {
if (!isDev) {
const engine = '/var/run/docker.sock';
const { id } = await prisma.destinationDocker.findFirst({
where: { engine, network: 'coolify' }
});
const { found } = await checkContainer({ dockerId: id, container: 'coolify-fluentbit', remove: true });
if (!found) {
await asyncExecShell(`env | grep '^COOLIFY' > .env`);
await asyncExecShell(`docker compose up -d fluent-bit`);
}
}
} catch (error) {
console.log(error)
}
}
async function checkProxies() {
try {
const { default: isReachable } = await import('is-port-reachable');
let portReachable;
const { arch, ipv4, ipv6 } = await listSettings();
// Coolify Proxy local
const engine = '/var/run/docker.sock';
const localDocker = await prisma.destinationDocker.findFirst({
where: { engine, network: 'coolify', isCoolifyProxyUsed: true }
});
if (localDocker) {
portReachable = await isReachable(80, { host: ipv4 || ipv6 })
if (!portReachable) {
await startTraefikProxy(localDocker.id);
}
}
// Coolify Proxy remote
const remoteDocker = await prisma.destinationDocker.findMany({
where: { remoteEngine: true, remoteVerified: true }
});
if (remoteDocker.length > 0) {
for (const docker of remoteDocker) {
if (docker.isCoolifyProxyUsed) {
portReachable = await isReachable(80, { host: docker.remoteIpAddress })
if (!portReachable) {
await startTraefikProxy(docker.id);
}
}
try {
await createRemoteEngineConfiguration(docker.id)
} catch (error) { }
}
}
// TCP Proxies
const databasesWithPublicPort = await prisma.database.findMany({
where: { publicPort: { not: null } },
include: { settings: true, destinationDocker: true }
});
for (const database of databasesWithPublicPort) {
const { destinationDockerId, destinationDocker, publicPort, id } = database;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
const { privatePort } = generateDatabaseConfiguration(database, arch);
await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
}
}
const wordpressWithFtp = await prisma.wordpress.findMany({
where: { ftpPublicPort: { not: null } },
include: { service: { include: { destinationDocker: true } } }
});
for (const ftp of wordpressWithFtp) {
const { service, ftpPublicPort } = ftp;
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
await startTraefikTCPProxy(destinationDocker, id, ftpPublicPort, 22, 'wordpressftp');
}
}
// HTTP Proxies
const minioInstances = await prisma.minio.findMany({
where: { publicPort: { not: null } },
include: { service: { include: { destinationDocker: true } } }
});
for (const minio of minioInstances) {
const { service, publicPort } = minio;
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000);
}
}
} catch (error) {
}
}
async function copySSLCertificates() {
try {
const pAll = await import('p-all');
const actions = []
const certificates = await prisma.certificate.findMany({ include: { team: true } })
const teamIds = certificates.map(c => c.teamId)
const destinations = await prisma.destinationDocker.findMany({ where: { isCoolifyProxyUsed: true, teams: { some: { id: { in: [...teamIds] } } } } })
for (const certificate of certificates) {
const { id, key, cert } = certificate
const decryptedKey = decrypt(key)
await fs.writeFile(`/tmp/${id}-key.pem`, decryptedKey)
await fs.writeFile(`/tmp/${id}-cert.pem`, cert)
for (const destination of destinations) {
if (destination.remoteEngine) {
if (destination.remoteVerified) {
const { id: dockerId, remoteIpAddress } = destination
actions.push(async () => copyRemoteCertificates(id, dockerId, remoteIpAddress))
}
} else {
actions.push(async () => copyLocalCertificates(id))
}
}
}
await pAll.default(actions, { concurrency: 1 })
} catch (error) {
console.log(error)
} finally {
await asyncExecShell(`find /tmp/ -maxdepth 1 -type f -name '*-*.pem' -delete`)
}
}
async function copyRemoteCertificates(id: string, dockerId: string, remoteIpAddress: string) {
try {
await asyncExecShell(`scp /tmp/${id}-cert.pem /tmp/${id}-key.pem ${remoteIpAddress}:/tmp/`)
await executeSSHCmd({ dockerId, command: `docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'` })
await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/` })
await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/` })
} catch (error) {
console.log({ error })
}
}
async function copyLocalCertificates(id: string) {
try {
await asyncExecShell(`docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'`)
await asyncExecShell(`docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/`)
await asyncExecShell(`docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/`)
} catch (error) {
console.log({ error })
}
}
async function cleanupStorage() {
const destinationDockers = await prisma.destinationDocker.findMany();
let enginesDone = new Set()
for (const destination of destinationDockers) {
if (enginesDone.has(destination.engine) || enginesDone.has(destination.remoteIpAddress)) return
if (destination.engine) enginesDone.add(destination.engine)
if (destination.remoteIpAddress) enginesDone.add(destination.remoteIpAddress)
let lowDiskSpace = false;
try {
let stdout = null
if (!isDev) {
const output = await executeDockerCmd({ dockerId: destination.id, command: `CONTAINER=$(docker ps -lq | head -1) && docker exec $CONTAINER sh -c 'df -kPT /'` })
stdout = output.stdout;
} else {
const output = await asyncExecShell(
`df -kPT /`
);
stdout = output.stdout;
}
let lines = stdout.trim().split('\n');
let header = lines[0];
let regex =
/^Filesystem\s+|Type\s+|1024-blocks|\s+Used|\s+Available|\s+Capacity|\s+Mounted on\s*$/g;
const boundaries = [];
let match;
while ((match = regex.exec(header))) {
boundaries.push(match[0].length);
}
boundaries[boundaries.length - 1] = -1;
const data = lines.slice(1).map((line) => {
const cl = boundaries.map((boundary) => {
const column = boundary > 0 ? line.slice(0, boundary) : line;
line = line.slice(boundary);
return column.trim();
});
return {
capacity: Number.parseInt(cl[5], 10) / 100
};
});
if (data.length > 0) {
const { capacity } = data[0];
if (capacity > 0.8) {
lowDiskSpace = true;
}
}
} catch (error) { }
await cleanupDockerStorage(destination.id, lowDiskSpace, false)
}
}

View File

@ -85,6 +85,7 @@ import * as buildpacks from '../lib/buildPacks';
baseDirectory, baseDirectory,
publishDirectory, publishDirectory,
dockerFileLocation, dockerFileLocation,
dockerComposeConfiguration,
denoMainFile denoMainFile
} = application } = application
const currentHash = crypto const currentHash = crypto
@ -112,17 +113,6 @@ import * as buildpacks from '../lib/buildPacks';
) )
.digest('hex'); .digest('hex');
const { debug } = settings; const { debug } = settings;
// if (concurrency === 1) {
// await prisma.build.updateMany({
// where: {
// status: { in: ['queued', 'running'] },
// id: { not: buildId },
// applicationId,
// createdAt: { lt: new Date(new Date().getTime() - 10 * 1000) }
// },
// data: { status: 'failed' }
// });
// }
let imageId = applicationId; let imageId = applicationId;
let domain = getDomain(fqdn); let domain = getDomain(fqdn);
const volumes = const volumes =
@ -138,6 +128,10 @@ import * as buildpacks from '../lib/buildPacks';
repository = sourceRepository || repository; repository = sourceRepository || repository;
} }
try {
dockerComposeConfiguration = JSON.parse(dockerComposeConfiguration)
} catch (error) { }
let deployNeeded = true; let deployNeeded = true;
let destinationType; let destinationType;
@ -212,17 +206,37 @@ import * as buildpacks from '../lib/buildPacks';
// //
} }
await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId, baseImage); await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId, baseImage);
const labels = makeLabelForStandaloneApplication({
applicationId,
fqdn,
name,
type,
pullmergeRequestId,
buildPack,
repository,
branch,
projectId,
port: exposePort ? `${exposePort}:${port}` : port,
commit,
installCommand,
buildCommand,
startCommand,
baseDirectory,
publishDirectory
});
if (forceRebuild) deployNeeded = true if (forceRebuild) deployNeeded = true
if (!imageFound || deployNeeded) { if (!imageFound || deployNeeded) {
// if (true) {
if (buildpacks[buildPack]) if (buildpacks[buildPack])
await buildpacks[buildPack]({ await buildpacks[buildPack]({
dockerId: destinationDocker.id, dockerId: destinationDocker.id,
network: destinationDocker.network,
buildId, buildId,
applicationId, applicationId,
domain, domain,
name, name,
type, type,
volumes,
labels,
pullmergeRequestId, pullmergeRequestId,
buildPack, buildPack,
repository, repository,
@ -244,11 +258,12 @@ import * as buildpacks from '../lib/buildPacks';
pythonModule, pythonModule,
pythonVariable, pythonVariable,
dockerFileLocation, dockerFileLocation,
dockerComposeConfiguration,
denoMainFile, denoMainFile,
denoOptions, denoOptions,
baseImage, baseImage,
baseBuildImage, baseBuildImage,
deploymentType deploymentType,
}); });
else { else {
await saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId }); await saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId });
@ -257,9 +272,53 @@ import * as buildpacks from '../lib/buildPacks';
} else { } else {
await saveBuildLog({ line: 'Build image already available - no rebuild required.', buildId, applicationId }); await saveBuildLog({ line: 'Build image already available - no rebuild required.', buildId, applicationId });
} }
if (buildPack === 'compose') {
try { try {
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker stop -t 0 ${imageId}` }) await executeDockerCmd({
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker rm ${imageId}` }) dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=coolify.applicationId=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0`
})
await executeDockerCmd({
dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=coolify.applicationId=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker rm --force`
})
} catch (error) {
//
}
try {
await executeDockerCmd({ debug, buildId, applicationId, dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` })
await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId });
await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } });
await prisma.application.update({
where: { id: applicationId },
data: { configHash: currentHash }
});
} catch (error) {
await saveBuildLog({ line: error, buildId, applicationId });
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } })
if (foundBuild) {
await prisma.build.update({
where: { id: buildId },
data: {
status: 'failed'
}
});
}
throw new Error(error);
}
} else {
try {
await executeDockerCmd({
dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=com.docker.compose.service=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0`
})
await executeDockerCmd({
dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=com.docker.compose.service=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker rm --force`
})
} catch (error) { } catch (error) {
// //
} }
@ -283,24 +342,7 @@ import * as buildpacks from '../lib/buildPacks';
}); });
} }
await fs.writeFile(`${workdir}/.env`, envs.join('\n')); await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
const labels = makeLabelForStandaloneApplication({
applicationId,
fqdn,
name,
type,
pullmergeRequestId,
buildPack,
repository,
branch,
projectId,
port: exposePort ? `${exposePort}:${port}` : port,
commit,
installCommand,
buildCommand,
startCommand,
baseDirectory,
publishDirectory
});
let envFound = false; let envFound = false;
try { try {
envFound = !!(await fs.stat(`${workdir}/.env`)); envFound = !!(await fs.stat(`${workdir}/.env`));
@ -328,9 +370,6 @@ import * as buildpacks from '../lib/buildPacks';
depends_on: [], depends_on: [],
expose: [port], expose: [port],
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
// logging: {
// driver: 'fluentd',
// },
...defaultComposeConfiguration(destinationDocker.network), ...defaultComposeConfiguration(destinationDocker.network),
} }
}, },
@ -365,6 +404,7 @@ import * as buildpacks from '../lib/buildPacks';
}); });
} }
} }
}
catch (error) { catch (error) {
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } }) const foundBuild = await prisma.build.findUnique({ where: { id: buildId } })
if (foundBuild) { if (foundBuild) {

View File

@ -1,296 +0,0 @@
import { parentPort } from 'node:worker_threads';
import axios from 'axios';
import { compareVersions } from 'compare-versions';
import { asyncExecShell, cleanupDockerStorage, executeDockerCmd, isDev, prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, listSettings, version, createRemoteEngineConfiguration, decrypt, executeSSHCmd } from '../lib/common';
import { checkContainer } from '../lib/docker';
import fs from 'fs/promises'
async function autoUpdater() {
try {
const currentVersion = version;
const { data: versions } = await axios
.get(
`https://get.coollabs.io/versions.json`
, {
params: {
appId: process.env['COOLIFY_APP_ID'] || undefined,
version: currentVersion
}
})
const latestVersion = versions['coolify'].main.version;
const isUpdateAvailable = compareVersions(latestVersion, currentVersion);
if (isUpdateAvailable === 1) {
const activeCount = 0
if (activeCount === 0) {
if (!isDev) {
const { isAutoUpdateEnabled } = await prisma.setting.findFirst();
if (isAutoUpdateEnabled) {
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
await asyncExecShell(`env | grep COOLIFY > .env`);
await asyncExecShell(
`sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env`
);
await asyncExecShell(
`docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"`
);
}
} else {
console.log('Updating (not really in dev mode).');
}
}
}
} catch (error) { }
}
async function checkFluentBit() {
if (!isDev) {
const engine = '/var/run/docker.sock';
const { id } = await prisma.destinationDocker.findFirst({
where: { engine, network: 'coolify' }
});
const { found } = await checkContainer({ dockerId: id, container: 'coolify-fluentbit' });
if (!found) {
await asyncExecShell(`env | grep COOLIFY > .env`);
await asyncExecShell(`docker compose up -d fluent-bit`);
}
}
}
async function copyRemoteCertificates(id: string, dockerId: string, remoteIpAddress: string) {
try {
await asyncExecShell(`scp /tmp/${id}-cert.pem /tmp/${id}-key.pem ${remoteIpAddress}:/tmp/`)
await executeSSHCmd({ dockerId, command: `docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'` })
await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/` })
await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/` })
} catch (error) {
console.log({ error })
}
}
async function copyLocalCertificates(id: string) {
try {
await asyncExecShell(`docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'`)
await asyncExecShell(`docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/`)
await asyncExecShell(`docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/`)
} catch (error) {
console.log({ error })
}
}
async function copySSLCertificates() {
try {
const pAll = await import('p-all');
const actions = []
const certificates = await prisma.certificate.findMany({ include: { team: true } })
const teamIds = certificates.map(c => c.teamId)
const destinations = await prisma.destinationDocker.findMany({ where: { isCoolifyProxyUsed: true, teams: { some: { id: { in: [...teamIds] } } } } })
for (const certificate of certificates) {
const { id, key, cert } = certificate
const decryptedKey = decrypt(key)
await fs.writeFile(`/tmp/${id}-key.pem`, decryptedKey)
await fs.writeFile(`/tmp/${id}-cert.pem`, cert)
for (const destination of destinations) {
if (destination.remoteEngine) {
if (destination.remoteVerified) {
const { id: dockerId, remoteIpAddress } = destination
actions.push(async () => copyRemoteCertificates(id, dockerId, remoteIpAddress))
}
} else {
actions.push(async () => copyLocalCertificates(id))
}
}
}
await pAll.default(actions, { concurrency: 1 })
} catch (error) {
console.log(error)
} finally {
await asyncExecShell(`find /tmp/ -maxdepth 1 -type f -name '*-*.pem' -delete`)
}
}
async function checkProxies() {
try {
const { default: isReachable } = await import('is-port-reachable');
let portReachable;
const { arch, ipv4, ipv6 } = await listSettings();
// Coolify Proxy local
const engine = '/var/run/docker.sock';
const localDocker = await prisma.destinationDocker.findFirst({
where: { engine, network: 'coolify', isCoolifyProxyUsed: true }
});
if (localDocker) {
portReachable = await isReachable(80, { host: ipv4 || ipv6 })
if (!portReachable) {
await startTraefikProxy(localDocker.id);
}
}
// Coolify Proxy remote
const remoteDocker = await prisma.destinationDocker.findMany({
where: { remoteEngine: true, remoteVerified: true }
});
if (remoteDocker.length > 0) {
for (const docker of remoteDocker) {
if (docker.isCoolifyProxyUsed) {
portReachable = await isReachable(80, { host: docker.remoteIpAddress })
if (!portReachable) {
await startTraefikProxy(docker.id);
}
}
try {
await createRemoteEngineConfiguration(docker.id)
} catch (error) { }
}
}
// TCP Proxies
const databasesWithPublicPort = await prisma.database.findMany({
where: { publicPort: { not: null } },
include: { settings: true, destinationDocker: true }
});
for (const database of databasesWithPublicPort) {
const { destinationDockerId, destinationDocker, publicPort, id } = database;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
const { privatePort } = generateDatabaseConfiguration(database, arch);
await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
}
}
const wordpressWithFtp = await prisma.wordpress.findMany({
where: { ftpPublicPort: { not: null } },
include: { service: { include: { destinationDocker: true } } }
});
for (const ftp of wordpressWithFtp) {
const { service, ftpPublicPort } = ftp;
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
await startTraefikTCPProxy(destinationDocker, id, ftpPublicPort, 22, 'wordpressftp');
}
}
// HTTP Proxies
const minioInstances = await prisma.minio.findMany({
where: { publicPort: { not: null } },
include: { service: { include: { destinationDocker: true } } }
});
for (const minio of minioInstances) {
const { service, publicPort } = minio;
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000);
}
}
} catch (error) {
}
}
async function cleanupPrismaEngines() {
if (!isDev) {
try {
const { stdout } = await asyncExecShell(`ps -ef | grep /app/prisma-engines/query-engine | grep -v grep | wc -l | xargs`)
if (stdout.trim() != null && stdout.trim() != '' && Number(stdout.trim()) > 1) {
await asyncExecShell(`killall -q -e /app/prisma-engines/query-engine -o 1m`)
}
} catch (error) { }
}
}
async function cleanupStorage() {
const destinationDockers = await prisma.destinationDocker.findMany();
let enginesDone = new Set()
for (const destination of destinationDockers) {
if (enginesDone.has(destination.engine) || enginesDone.has(destination.remoteIpAddress)) return
if (destination.engine) enginesDone.add(destination.engine)
if (destination.remoteIpAddress) enginesDone.add(destination.remoteIpAddress)
let lowDiskSpace = false;
try {
let stdout = null
if (!isDev) {
const output = await executeDockerCmd({ dockerId: destination.id, command: `CONTAINER=$(docker ps -lq | head -1) && docker exec $CONTAINER sh -c 'df -kPT /'` })
stdout = output.stdout;
} else {
const output = await asyncExecShell(
`df -kPT /`
);
stdout = output.stdout;
}
let lines = stdout.trim().split('\n');
let header = lines[0];
let regex =
/^Filesystem\s+|Type\s+|1024-blocks|\s+Used|\s+Available|\s+Capacity|\s+Mounted on\s*$/g;
const boundaries = [];
let match;
while ((match = regex.exec(header))) {
boundaries.push(match[0].length);
}
boundaries[boundaries.length - 1] = -1;
const data = lines.slice(1).map((line) => {
const cl = boundaries.map((boundary) => {
const column = boundary > 0 ? line.slice(0, boundary) : line;
line = line.slice(boundary);
return column.trim();
});
return {
capacity: Number.parseInt(cl[5], 10) / 100
};
});
if (data.length > 0) {
const { capacity } = data[0];
if (capacity > 0.8) {
lowDiskSpace = true;
}
}
} catch (error) { }
await cleanupDockerStorage(destination.id, lowDiskSpace, false)
}
}
(async () => {
let status = {
cleanupStorage: false,
autoUpdater: false,
copySSLCertificates: false,
}
if (parentPort) {
parentPort.on('message', async (message) => {
if (parentPort) {
if (message === 'error') throw new Error('oops');
if (message === 'cancel') {
parentPort.postMessage('cancelled');
process.exit(1);
}
if (message === 'action:cleanupStorage') {
if (!status.autoUpdater) {
status.cleanupStorage = true
await cleanupStorage();
status.cleanupStorage = false
}
return;
}
if (message === 'action:cleanupPrismaEngines') {
await cleanupPrismaEngines();
return;
}
if (message === 'action:checkProxies') {
await checkProxies();
return;
}
if (message === 'action:checkFluentBit') {
await checkFluentBit();
return;
}
if (message === 'action:copySSLCertificates') {
if (!status.copySSLCertificates) {
status.copySSLCertificates = true
await copySSLCertificates();
status.copySSLCertificates = false
}
return;
}
if (message === 'action:autoUpdater') {
if (!status.cleanupStorage) {
status.autoUpdater = true
await autoUpdater();
status.autoUpdater = false
}
return;
}
}
});
} else process.exit(0);
})();

View File

@ -468,9 +468,9 @@ export const saveBuildLog = async ({
line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@'); line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
} }
const addTimestamp = `[${generateTimestamp()}] ${line}`; const addTimestamp = `[${generateTimestamp()}] ${line}`;
const fluentBitUrl = isDev ? 'http://localhost:24224' : 'http://coolify-fluentbit:24224'; const fluentBitUrl = isDev ? process.env.COOLIFY_CONTAINER_DEV === 'true' ? 'http://coolify-fluentbit:24224' : 'http://localhost:24224' : 'http://coolify-fluentbit:24224';
if (isDev) { if (isDev && !process.env.COOLIFY_CONTAINER_DEV) {
console.debug(`[${applicationId}] ${addTimestamp}`); console.debug(`[${applicationId}] ${addTimestamp}`);
} }
try { try {
@ -580,7 +580,8 @@ export async function buildImage({
dockerId, dockerId,
isCache = false, isCache = false,
debug = false, debug = false,
dockerFileLocation = '/Dockerfile' dockerFileLocation = '/Dockerfile',
commit
}) { }) {
if (isCache) { if (isCache) {
await saveBuildLog({ line: `Building cache image started.`, buildId, applicationId }); await saveBuildLog({ line: `Building cache image started.`, buildId, applicationId });
@ -596,7 +597,9 @@ export async function buildImage({
} }
const dockerFile = isCache ? `${dockerFileLocation}-cache` : `${dockerFileLocation}` const dockerFile = isCache ? `${dockerFileLocation}-cache` : `${dockerFileLocation}`
const cache = `${applicationId}:${tag}${isCache ? '-cache' : ''}` const cache = `${applicationId}:${tag}${isCache ? '-cache' : ''}`
await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker build --progress plain -f ${workdir}/${dockerFile} -t ${cache} ${workdir}` })
await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker build --progress plain -f ${workdir}/${dockerFile} -t ${cache} --build-arg SOURCE_COMMIT=${commit} ${workdir}` })
const { status } = await prisma.build.findUnique({ where: { id: buildId } }) const { status } = await prisma.build.findUnique({ where: { id: buildId } })
if (status === 'canceled') { if (status === 'canceled') {
throw new Error('Deployment canceled.') throw new Error('Deployment canceled.')
@ -634,6 +637,7 @@ export function makeLabelForStandaloneApplication({
return [ return [
'coolify.managed=true', 'coolify.managed=true',
`coolify.version=${version}`, `coolify.version=${version}`,
`coolify.applicationId=${applicationId}`,
`coolify.type=standalone-application`, `coolify.type=standalone-application`,
`coolify.configuration=${base64Encode( `coolify.configuration=${base64Encode(
JSON.stringify({ JSON.stringify({

View File

@ -0,0 +1,100 @@
import { promises as fs } from 'fs';
import { defaultComposeConfiguration, executeDockerCmd } from '../common';
import { buildImage, saveBuildLog } from './common';
import yaml from 'js-yaml';
export default async function (data) {
let {
applicationId,
debug,
buildId,
dockerId,
network,
volumes,
labels,
workdir,
baseDirectory,
secrets,
pullmergeRequestId,
port,
dockerComposeConfiguration
} = data
const fileYml = `${workdir}${baseDirectory}/docker-compose.yml`;
const fileYaml = `${workdir}${baseDirectory}/docker-compose.yaml`;
let dockerComposeRaw = null;
let isYml = false;
try {
dockerComposeRaw = await fs.readFile(`${fileYml}`, 'utf8')
isYml = true
} catch (error) { }
try {
dockerComposeRaw = await fs.readFile(`${fileYaml}`, 'utf8')
} catch (error) { }
if (!dockerComposeRaw) {
throw ('docker-compose.yml or docker-compose.yaml are not found!');
}
const dockerComposeYaml = yaml.load(dockerComposeRaw)
if (!dockerComposeYaml.services) {
throw 'No Services found in docker-compose file.'
}
const envs = [
`PORT=${port}`
];
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (pullmergeRequestId) {
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
envs.push(`${secret.name}=${isSecretFound[0].value}`);
} else {
envs.push(`${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
envs.push(`${secret.name}=${secret.value}`);
}
}
});
}
await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
let envFound = false;
try {
envFound = !!(await fs.stat(`${workdir}/.env`));
} catch (error) {
//
}
const composeVolumes = volumes.map((volume) => {
return {
[`${volume.split(':')[0]}`]: {
name: volume.split(':')[0]
}
};
});
let networks = {}
for (let [key, value] of Object.entries(dockerComposeYaml.services)) {
value['container_name'] = `${applicationId}-${key}`
value['env_file'] = envFound ? [`${workdir}/.env`] : []
value['labels'] = labels
value['volumes'] = volumes
if (dockerComposeConfiguration[key].port) {
value['expose'] = [dockerComposeConfiguration[key].port]
}
if (value['networks']?.length > 0) {
value['networks'].forEach((network) => {
networks[network] = {
name: network
}
})
}
value['networks'] = [...value['networks'] || '', network]
dockerComposeYaml.services[key] = { ...dockerComposeYaml.services[key], restart: defaultComposeConfiguration(network).restart, deploy: defaultComposeConfiguration(network).deploy }
}
dockerComposeYaml['volumes'] = Object.assign({ ...dockerComposeYaml['volumes'] }, ...composeVolumes)
dockerComposeYaml['networks'] = Object.assign({ ...networks }, { [network]: { external: true } })
await fs.writeFile(`${workdir}/docker-compose.${isYml ? 'yml' : 'yaml'}`, yaml.dump(dockerComposeYaml));
await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker compose --project-directory ${workdir} pull` })
await saveBuildLog({ line: 'Pulling images from Compose file.', buildId, applicationId });
await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker compose --project-directory ${workdir} build --progress plain` })
await saveBuildLog({ line: 'Building images from Compose file.', buildId, applicationId });
}

View File

@ -49,7 +49,7 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push(`RUN deno cache ${denoMainFile}`); Dockerfile.push(`RUN deno cache ${denoMainFile}`);
Dockerfile.push(`ENV NO_COLOR true`); Dockerfile.push(`ENV NO_COLOR true`);
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push(`CMD deno run ${denoOptions ? denoOptions.split(' ') : ''} ${denoMainFile}`); Dockerfile.push(`CMD deno run ${denoOptions || ''} ${denoMainFile}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };

View File

@ -16,6 +16,7 @@ import python from './python';
import deno from './deno'; import deno from './deno';
import laravel from './laravel'; import laravel from './laravel';
import heroku from './heroku'; import heroku from './heroku';
import compose from './compose'
export { export {
node, node,
@ -35,5 +36,6 @@ export {
python, python,
deno, deno,
laravel, laravel,
heroku heroku,
compose
}; };

View File

@ -20,7 +20,7 @@ import { scheduler } from './scheduler';
import { supportedServiceTypesAndVersions } from './services/supportedVersions'; import { supportedServiceTypesAndVersions } from './services/supportedVersions';
import { includeServices } from './services/common'; import { includeServices } from './services/common';
export const version = '3.10.14'; export const version = '3.10.15';
export const isDev = process.env.NODE_ENV === 'development'; export const isDev = process.env.NODE_ENV === 'development';
const algorithm = 'aes-256-ctr'; const algorithm = 'aes-256-ctr';
@ -264,7 +264,9 @@ export async function isDomainConfigured({
where: { where: {
OR: [ OR: [
{ fqdn: { endsWith: `//${nakedDomain}` } }, { fqdn: { endsWith: `//${nakedDomain}` } },
{ fqdn: { endsWith: `//www.${nakedDomain}` } } { fqdn: { endsWith: `//www.${nakedDomain}` } },
{ dockerComposeConfiguration: { contains: `//${nakedDomain}` } },
{ dockerComposeConfiguration: { contains: `//www.${nakedDomain}` } }
], ],
id: { not: id }, id: { not: id },
destinationDocker: { destinationDocker: {
@ -598,7 +600,7 @@ export async function executeDockerCmd({ debug, buildId, applicationId, dockerId
command = command.replace(/docker compose/gi, 'docker-compose'); command = command.replace(/docker compose/gi, 'docker-compose');
} }
} }
if (command.startsWith(`docker build --progress plain`) || command.startsWith(`pack build`)) { if (command.startsWith(`docker build`) || command.startsWith(`pack build`) || command.startsWith(`docker compose build`)) {
return await asyncExecShellStream({ debug, buildId, applicationId, command, engine }); return await asyncExecShellStream({ debug, buildId, applicationId, command, engine });
} }
return await execaCommand(command, { env: { DOCKER_BUILDKIT: "1", DOCKER_HOST: engine }, shell: true }) return await execaCommand(command, { env: { DOCKER_BUILDKIT: "1", DOCKER_HOST: engine }, shell: true })

View File

@ -87,6 +87,9 @@ export async function removeContainer({
await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` }) await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` })
await executeDockerCmd({ dockerId, command: `docker rm ${id}` }) await executeDockerCmd({ dockerId, command: `docker rm ${id}` })
} }
if (JSON.parse(stdout).Status === 'exited') {
await executeDockerCmd({ dockerId, command: `docker rm ${id}` })
}
} catch (error) { } catch (error) {
throw error; throw error;
} }

View File

@ -73,6 +73,4 @@ export default async function ({
const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`); const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`);
return commit.replace('\n', ''); return commit.replace('\n', '');
} }

View File

@ -11,15 +11,14 @@ const options: any = {
defaultExtension: 'js', defaultExtension: 'js',
logger: new Cabin(), logger: new Cabin(),
// logger: false, // logger: false,
workerMessageHandler: async ({ name, message }) => { // workerMessageHandler: async ({ name, message }) => {
if (name === 'deployApplication' && message?.deploying) { // if (name === 'deployApplication' && message?.deploying) {
if (scheduler.workers.has('autoUpdater') || scheduler.workers.has('cleanupStorage')) { // if (scheduler.workers.has('autoUpdater') || scheduler.workers.has('cleanupStorage')) {
scheduler.workers.get('deployApplication').postMessage('cancel') // scheduler.workers.get('deployApplication').postMessage('cancel')
} // }
} // }
}, // },
jobs: [ jobs: [
{ name: 'infrastructure' },
{ name: 'deployApplication' }, { name: 'deployApplication' },
], ],
}; };

View File

@ -1410,6 +1410,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
depends_on: [ depends_on: [
`${id}-mariadb`, `${id}-mariadb`,
`${id}-redis`, `${id}-redis`,
`${id}-influxdb`,
], ],
environment: [ environment: [
"_APP_ENV=production", "_APP_ENV=production",
@ -1772,14 +1773,11 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
], ],
...defaultComposeConfiguration(network), ...defaultComposeConfiguration(network),
}, },
[`${id}-usage-timeseries`]: {
};
dockerCompose[id].depends_on.push(`${id}-influxdb`);
dockerCompose[`${id}-usage`] = {
image: `${image}:${version}`, image: `${image}:${version}`,
container_name: `${id}-usage`, container_name: `${id}-usage`,
labels: makeLabelForServices('appwrite'), labels: makeLabelForServices('appwrite'),
entrypoint: "usage", entrypoint: "usage --type=timeseries",
depends_on: [ depends_on: [
`${id}-mariadb`, `${id}-mariadb`,
`${id}-influxdb`, `${id}-influxdb`,
@ -1800,16 +1798,42 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
...secrets ...secrets
], ],
...defaultComposeConfiguration(network), ...defaultComposeConfiguration(network),
} },
dockerCompose[`${id}-influxdb`] = { [`${id}-usage-database`]: {
image: `${image}:${version}`,
container_name: `${id}-usage-database`,
labels: makeLabelForServices('appwrite'),
entrypoint: "usage --type=database",
depends_on: [
`${id}-mariadb`,
`${id}-influxdb`,
],
environment: [
"_APP_ENV=production",
`_APP_OPENSSL_KEY_V1=${opensslKeyV1}`,
`_APP_DB_HOST=${mariadbHost}`,
`_APP_DB_PORT=${mariadbPort}`,
`_APP_DB_SCHEMA=${mariadbDatabase}`,
`_APP_DB_USER=${mariadbUser}`,
`_APP_DB_PASS=${mariadbPassword}`,
`_APP_INFLUXDB_HOST=${id}-influxdb`,
"_APP_INFLUXDB_PORT=8086",
`_APP_REDIS_HOST=${id}-redis`,
"_APP_REDIS_PORT=6379",
`OPEN_RUNTIMES_NETWORK=${network}`,
...secrets
],
...defaultComposeConfiguration(network),
},
[`${id}-influxdb`]: {
image: "appwrite/influxdb:1.5.0", image: "appwrite/influxdb:1.5.0",
container_name: `${id}-influxdb`, container_name: `${id}-influxdb`,
volumes: [ volumes: [
`${id}-influxdb:/var/lib/influxdb:rw` `${id}-influxdb:/var/lib/influxdb:rw`
], ],
...defaultComposeConfiguration(network), ...defaultComposeConfiguration(network),
} },
dockerCompose[`${id}-telegraf`] = { [`${id}-telegraf`]: {
image: "appwrite/telegraf:1.4.0", image: "appwrite/telegraf:1.4.0",
container_name: `${id}-telegraf`, container_name: `${id}-telegraf`,
environment: [ environment: [
@ -1819,7 +1843,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
], ],
...defaultComposeConfiguration(network), ...defaultComposeConfiguration(network),
} }
};
const composeFile: any = { const composeFile: any = {
version: '3.8', version: '3.8',
services: dockerCompose, services: dockerCompose,
@ -1868,7 +1892,9 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
} }
} }
async function startServiceContainers(dockerId, composeFileDestination) { async function startServiceContainers(dockerId, composeFileDestination) {
try {
await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} pull` }) await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} pull` })
} catch (error) { }
await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} build --no-cache` }) await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} build --no-cache` })
await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} create` }) await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} create` })
await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} start` }) await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} start` })

View File

@ -29,7 +29,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'stable', recommendedVersion: 'stable',
ports: { ports: {
main: 8000 main: 8000
} },
labels: ['analytics', 'plausible', 'plausible-analytics', 'gdpr', 'no-cookie']
}, },
{ {
name: 'nocodb', name: 'nocodb',
@ -39,7 +40,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'latest', recommendedVersion: 'latest',
ports: { ports: {
main: 8080 main: 8080
} },
labels: ['nocodb', 'airtable', 'database']
}, },
{ {
name: 'minio', name: 'minio',
@ -49,7 +51,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'latest', recommendedVersion: 'latest',
ports: { ports: {
main: 9001 main: 9001
} },
labels: ['minio', 's3', 'storage']
}, },
{ {
name: 'vscodeserver', name: 'vscodeserver',
@ -59,7 +62,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'latest', recommendedVersion: 'latest',
ports: { ports: {
main: 8080 main: 8080
} },
labels: ['vscodeserver', 'vscode', 'code-server', 'ide']
}, },
{ {
name: 'wordpress', name: 'wordpress',
@ -70,7 +74,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'latest', recommendedVersion: 'latest',
ports: { ports: {
main: 80 main: 80
} },
labels: ['wordpress', 'blog', 'cms']
}, },
{ {
name: 'vaultwarden', name: 'vaultwarden',
@ -80,7 +85,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'latest', recommendedVersion: 'latest',
ports: { ports: {
main: 80 main: 80
} },
labels: ['vaultwarden', 'password-manager', 'passwords']
}, },
{ {
name: 'languagetool', name: 'languagetool',
@ -90,7 +96,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'latest', recommendedVersion: 'latest',
ports: { ports: {
main: 8010 main: 8010
} },
labels: ['languagetool', 'grammar', 'spell-checker']
}, },
{ {
name: 'n8n', name: 'n8n',
@ -100,7 +107,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'latest', recommendedVersion: 'latest',
ports: { ports: {
main: 5678 main: 5678
} },
labels: ['n8n', 'workflow', 'automation', 'ifttt', 'zapier', 'nodered']
}, },
{ {
name: 'uptimekuma', name: 'uptimekuma',
@ -110,7 +118,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'latest', recommendedVersion: 'latest',
ports: { ports: {
main: 3001 main: 3001
} },
labels: ['uptimekuma', 'uptime', 'monitoring']
}, },
{ {
name: 'ghost', name: 'ghost',
@ -121,7 +130,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'latest', recommendedVersion: 'latest',
ports: { ports: {
main: 2368 main: 2368
} },
labels: ['ghost', 'blog', 'cms']
}, },
{ {
name: 'meilisearch', name: 'meilisearch',
@ -132,7 +142,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'latest', recommendedVersion: 'latest',
ports: { ports: {
main: 7700 main: 7700
} },
labels: ['meilisearch', 'search', 'search-engine']
}, },
{ {
name: 'umami', name: 'umami',
@ -143,7 +154,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'postgresql-latest', recommendedVersion: 'postgresql-latest',
ports: { ports: {
main: 3000 main: 3000
} },
labels: ['umami', 'analytics', 'gdpr', 'no-cookie']
}, },
{ {
name: 'hasura', name: 'hasura',
@ -154,7 +166,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'v2.10.0', recommendedVersion: 'v2.10.0',
ports: { ports: {
main: 8080 main: 8080
} },
labels: ['hasura', 'graphql', 'database']
}, },
{ {
name: 'fider', name: 'fider',
@ -165,7 +178,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'stable', recommendedVersion: 'stable',
ports: { ports: {
main: 3000 main: 3000
} },
labels: ['fider', 'feedback', 'suggestions']
}, },
{ {
name: 'appwrite', name: 'appwrite',
@ -176,7 +190,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: '1.0', recommendedVersion: '1.0',
ports: { ports: {
main: 80 main: 80
} },
labels: ['appwrite', 'database', 'storage', 'api', 'serverless']
}, },
// { // {
// name: 'moodle', // name: 'moodle',
@ -198,7 +213,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'latest', recommendedVersion: 'latest',
ports: { ports: {
main: 8000 main: 8000
} },
labels: ['glitchtip', 'error-reporting', 'error', 'sentry', 'bugsnag']
}, },
{ {
name: 'searxng', name: 'searxng',
@ -209,7 +225,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'latest', recommendedVersion: 'latest',
ports: { ports: {
main: 8080 main: 8080
} },
labels: ['searxng', 'search', 'search-engine']
}, },
{ {
name: 'weblate', name: 'weblate',
@ -220,7 +237,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'latest', recommendedVersion: 'latest',
ports: { ports: {
main: 8080 main: 8080
} },
labels: ['weblate', 'translation', 'localization']
}, },
// { // {
// name: 'taiga', // name: 'taiga',
@ -242,7 +260,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'latest', recommendedVersion: 'latest',
ports: { ports: {
main: 3000 main: 3000
} },
labels: ['grafana', 'monitoring', 'metrics', 'dashboard']
}, },
{ {
name: 'trilium', name: 'trilium',
@ -253,6 +272,7 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'latest', recommendedVersion: 'latest',
ports: { ports: {
main: 8080 main: 8080
} },
labels: ['trilium', 'notes', 'note-taking', 'wiki']
}, },
]; ];

View File

@ -110,23 +110,64 @@ export async function getApplicationStatus(request: FastifyRequest<OnlyId>) {
try { try {
const { id } = request.params const { id } = request.params
const { teamId } = request.user const { teamId } = request.user
let payload = []
const application: any = await getApplicationFromDB(id, teamId);
if (application?.destinationDockerId) {
if (application.buildPack === 'compose') {
const { stdout: containers } = await executeDockerCmd({
dockerId: application.destinationDocker.id,
command:
`docker ps -a --filter "label=coolify.applicationId=${id}" --format '{{json .}}'`
});
const containersArray = containers.trim().split('\n');
if (containersArray.length > 0 && containersArray[0] !== '') {
for (const container of containersArray) {
let isRunning = false;
let isExited = false;
let isRestarting = false;
const containerObj = JSON.parse(container);
const status = containerObj.State
if (status === 'running') {
isRunning = true;
}
if (status === 'exited') {
isExited = true;
}
if (status === 'restarting') {
isRestarting = true;
}
payload.push({
name: containerObj.Names,
status: {
isRunning,
isExited,
isRestarting
}
})
}
}
} else {
let isRunning = false; let isRunning = false;
let isExited = false; let isExited = false;
let isRestarting = false; let isRestarting = false;
const application: any = await getApplicationFromDB(id, teamId);
if (application?.destinationDockerId) {
const status = await checkContainer({ dockerId: application.destinationDocker.id, container: id }); const status = await checkContainer({ dockerId: application.destinationDocker.id, container: id });
if (status?.found) { if (status?.found) {
isRunning = status.status.isRunning; isRunning = status.status.isRunning;
isExited = status.status.isExited; isExited = status.status.isExited;
isRestarting = status.status.isRestarting isRestarting = status.status.isRestarting
} payload.push({
} name: id,
return { status: {
isRunning, isRunning,
isRestarting,
isExited, isExited,
}; isRestarting
}
})
}
}
}
return payload
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
@ -289,13 +330,15 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
baseImage, baseImage,
baseBuildImage, baseBuildImage,
deploymentType, deploymentType,
baseDatabaseBranch baseDatabaseBranch,
dockerComposeFile,
dockerComposeFileLocation,
dockerComposeConfiguration
} = request.body } = request.body
if (port) port = Number(port); if (port) port = Number(port);
if (exposePort) { if (exposePort) {
exposePort = Number(exposePort); exposePort = Number(exposePort);
} }
const { destinationDocker: { engine, remoteEngine, remoteIpAddress }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } }) const { destinationDocker: { engine, remoteEngine, remoteIpAddress }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } })
if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress }) if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress })
if (denoOptions) denoOptions = denoOptions.trim(); if (denoOptions) denoOptions = denoOptions.trim();
@ -324,6 +367,9 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
baseImage, baseImage,
baseBuildImage, baseBuildImage,
deploymentType, deploymentType,
dockerComposeFile,
dockerComposeFileLocation,
dockerComposeConfiguration,
...defaultConfiguration, ...defaultConfiguration,
connectedDatabase: { update: { hostedDatabaseDBName: baseDatabaseBranch } } connectedDatabase: { update: { hostedDatabaseDBName: baseDatabaseBranch } }
} }
@ -342,6 +388,9 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
baseImage, baseImage,
baseBuildImage, baseBuildImage,
deploymentType, deploymentType,
dockerComposeFile,
dockerComposeFileLocation,
dockerComposeConfiguration,
...defaultConfiguration ...defaultConfiguration
} }
}); });
@ -506,6 +555,21 @@ export async function stopApplication(request: FastifyRequest<OnlyId>, reply: Fa
const application: any = await getApplicationFromDB(id, teamId); const application: any = await getApplicationFromDB(id, teamId);
if (application?.destinationDockerId) { if (application?.destinationDockerId) {
const { id: dockerId } = application.destinationDocker; const { id: dockerId } = application.destinationDocker;
if (application.buildPack === 'compose') {
const { stdout: containers } = await executeDockerCmd({
dockerId: application.destinationDocker.id,
command:
`docker ps -a --filter "label=coolify.applicationId=${id}" --format '{{json .}}'`
});
const containersArray = containers.trim().split('\n');
if (containersArray.length > 0 && containersArray[0] !== '') {
for (const container of containersArray) {
const containerObj = JSON.parse(container);
await removeContainer({ id: containerObj.ID, dockerId: application.destinationDocker.id });
}
}
return
}
const { found } = await checkContainer({ dockerId, container: id }); const { found } = await checkContainer({ dockerId, container: id });
if (found) { if (found) {
await removeContainer({ id, dockerId: application.destinationDocker.id }); await removeContainer({ id, dockerId: application.destinationDocker.id });
@ -613,6 +677,24 @@ export async function getUsage(request) {
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function getUsageByContainer(request) {
try {
const { id, containerId } = request.params
const teamId = request.user?.teamId;
let usage = {};
const application: any = await getApplicationFromDB(id, teamId);
if (application.destinationDockerId) {
[usage] = await Promise.all([getContainerUsage(application.destinationDocker.id, containerId)]);
}
return {
usage
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function deployApplication(request: FastifyRequest<DeployApplication>) { export async function deployApplication(request: FastifyRequest<DeployApplication>) {
try { try {
const { id } = request.params const { id } = request.params
@ -1159,7 +1241,7 @@ export async function getPreviews(request: FastifyRequest<OnlyId>) {
export async function getApplicationLogs(request: FastifyRequest<GetApplicationLogs>) { export async function getApplicationLogs(request: FastifyRequest<GetApplicationLogs>) {
try { try {
const { id } = request.params; const { id, containerId } = request.params;
let { since = 0 } = request.query let { since = 0 } = request.query
if (since !== 0) { if (since !== 0) {
since = day(since).unix(); since = day(since).unix();
@ -1170,10 +1252,8 @@ export async function getApplicationLogs(request: FastifyRequest<GetApplicationL
}); });
if (destinationDockerId) { if (destinationDockerId) {
try { try {
// const found = await checkContainer({ dockerId, container: id })
// if (found) {
const { default: ansi } = await import('strip-ansi') const { default: ansi } = await import('strip-ansi')
const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${id}` }) const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${containerId}` })
const stripLogsStdout = stdout.toString().split('\n').map((l) => ansi(l)).filter((a) => a); const stripLogsStdout = stdout.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
const stripLogsStderr = stderr.toString().split('\n').map((l) => ansi(l)).filter((a) => a); const stripLogsStderr = stderr.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
const logs = stripLogsStderr.concat(stripLogsStdout) const logs = stripLogsStderr.concat(stripLogsStdout)
@ -1181,7 +1261,10 @@ export async function getApplicationLogs(request: FastifyRequest<GetApplicationL
return { logs: sortedLogs } return { logs: sortedLogs }
// } // }
} catch (error) { } catch (error) {
const { statusCode } = error; const { statusCode, stderr } = error;
if (stderr.startsWith('Error: No such container')) {
return { logs: [], noContainer: true }
}
if (statusCode === 404) { if (statusCode === 404) {
return { return {
logs: [] logs: []

View File

@ -1,6 +1,6 @@
import { FastifyPluginAsync } from 'fastify'; import { FastifyPluginAsync } from 'fastify';
import { OnlyId } from '../../../../types'; import { OnlyId } from '../../../../types';
import { cancelDeployment, checkDNS, checkDomain, checkRepository, cleanupUnconfiguredApplications, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildPack, getBuilds, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getPreviewStatus, getSecrets, getStorages, getUsage, listApplications, loadPreviews, newApplication, restartApplication, restartPreview, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveConnectedDatabase, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication, updatePreviewSecret, updateSecret } from './handlers'; import { cancelDeployment, checkDNS, checkDomain, checkRepository, cleanupUnconfiguredApplications, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildPack, getBuilds, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getPreviewStatus, getSecrets, getStorages, getUsage, getUsageByContainer, listApplications, loadPreviews, newApplication, restartApplication, restartPreview, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveConnectedDatabase, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication, updatePreviewSecret, updateSecret } from './handlers';
import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuilds, GetImages, RestartPreviewApplication, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types'; import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuilds, GetImages, RestartPreviewApplication, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types';
@ -45,11 +45,13 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get<RestartPreviewApplication>('/:id/previews/:pullmergeRequestId/status', async (request) => await getPreviewStatus(request)); fastify.get<RestartPreviewApplication>('/:id/previews/:pullmergeRequestId/status', async (request) => await getPreviewStatus(request));
fastify.post<RestartPreviewApplication>('/:id/previews/:pullmergeRequestId/restart', async (request, reply) => await restartPreview(request, reply)); fastify.post<RestartPreviewApplication>('/:id/previews/:pullmergeRequestId/restart', async (request, reply) => await restartPreview(request, reply));
fastify.get<GetApplicationLogs>('/:id/logs', async (request) => await getApplicationLogs(request)); // fastify.get<GetApplicationLogs>('/:id/logs', async (request) => await getApplicationLogs(request));
fastify.get<GetApplicationLogs>('/:id/logs/:containerId', async (request) => await getApplicationLogs(request));
fastify.get<GetBuilds>('/:id/logs/build', async (request) => await getBuilds(request)); fastify.get<GetBuilds>('/:id/logs/build', async (request) => await getBuilds(request));
fastify.get<GetBuildIdLogs>('/:id/logs/build/:buildId', async (request) => await getBuildIdLogs(request)); fastify.get<GetBuildIdLogs>('/:id/logs/build/:buildId', async (request) => await getBuildIdLogs(request));
fastify.get('/:id/usage', async (request) => await getUsage(request)) fastify.get('/:id/usage', async (request) => await getUsage(request))
fastify.get('/:id/usage/:containerId', async (request) => await getUsageByContainer(request))
fastify.post<DeployApplication>('/:id/deploy', async (request) => await deployApplication(request)) fastify.post<DeployApplication>('/:id/deploy', async (request) => await deployApplication(request))
fastify.post<CancelDeployment>('/:id/cancel', async (request, reply) => await cancelDeployment(request, reply)); fastify.post<CancelDeployment>('/:id/cancel', async (request, reply) => await cancelDeployment(request, reply));

View File

@ -21,7 +21,10 @@ export interface SaveApplication extends OnlyId {
baseImage: string, baseImage: string,
baseBuildImage: string, baseBuildImage: string,
deploymentType: string, deploymentType: string,
baseDatabaseBranch: string baseDatabaseBranch: string,
dockerComposeFile: string,
dockerComposeFileLocation: string,
dockerComposeConfiguration: string
} }
} }
export interface SaveApplicationSettings extends OnlyId { export interface SaveApplicationSettings extends OnlyId {
@ -84,7 +87,11 @@ export interface DeleteStorage extends OnlyId {
path: string, path: string,
} }
} }
export interface GetApplicationLogs extends OnlyId { export interface GetApplicationLogs {
Params: {
id: string,
containerId: string
}
Querystring: { Querystring: {
since: number, since: number,
} }

View File

@ -146,7 +146,7 @@ export async function showDashboard(request: FastifyRequest) {
let foundUnconfiguredApplication = false; let foundUnconfiguredApplication = false;
for (const application of applications) { for (const application of applications) {
if (!application.buildPack || !application.destinationDockerId || !application.branch || (!application.settings?.isBot && !application?.fqdn)) { if (!application.buildPack || !application.destinationDockerId || !application.branch || (!application.settings?.isBot && !application?.fqdn) && application.buildPack !== "compose") {
foundUnconfiguredApplication = true foundUnconfiguredApplication = true
} }
} }

View File

@ -234,6 +234,8 @@ export async function traefikConfiguration(request, reply) {
fqdn, fqdn,
id, id,
port, port,
buildPack,
dockerComposeConfiguration,
destinationDocker, destinationDocker,
destinationDockerId, destinationDockerId,
settings: { previews, dualCerts, isCustomSSL } settings: { previews, dualCerts, isCustomSSL }
@ -241,6 +243,33 @@ export async function traefikConfiguration(request, reply) {
if (destinationDockerId) { if (destinationDockerId) {
const { network, id: dockerId } = destinationDocker; const { network, id: dockerId } = destinationDocker;
const isRunning = true; const isRunning = true;
if (buildPack === 'compose') {
const services = Object.entries(JSON.parse(dockerComposeConfiguration))
for (const service of services) {
const [key, value] = service
const { port: customPort, fqdn } = value
if (fqdn) {
const domain = getDomain(fqdn);
const nakedDomain = domain.replace(/^www\./, '');
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
data.applications.push({
id: `${id}-${key}`,
container: `${id}-${key}`,
port: customPort ? customPort : port || 3000,
domain,
nakedDomain,
isRunning,
isHttps,
isWWW,
isDualCerts: dualCerts,
isCustomSSL
});
}
}
continue;
}
if (fqdn) { if (fqdn) {
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
const nakedDomain = domain.replace(/^www\./, ''); const nakedDomain = domain.replace(/^www\./, '');
@ -604,13 +633,41 @@ export async function remoteTraefikConfiguration(request: FastifyRequest<OnlyId>
fqdn, fqdn,
id, id,
port, port,
buildPack,
dockerComposeConfiguration,
destinationDocker, destinationDocker,
destinationDockerId, destinationDockerId,
settings: { previews, dualCerts } settings: { previews, dualCerts, isCustomSSL }
} = application; } = application;
if (destinationDockerId) { if (destinationDockerId) {
const { id: dockerId, network } = destinationDocker; const { id: dockerId, network } = destinationDocker;
const isRunning = true; const isRunning = true;
if (buildPack === 'compose') {
const services = Object.entries(JSON.parse(dockerComposeConfiguration))
for (const service of services) {
const [key, value] = service
const { port: customPort, fqdn } = value
if (fqdn) {
const domain = getDomain(fqdn);
const nakedDomain = domain.replace(/^www\./, '');
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
data.applications.push({
id: `${id}-${key}`,
container: `${id}-${key}`,
port: customPort ? customPort : port || 3000,
domain,
nakedDomain,
isRunning,
isHttps,
isWWW,
isDualCerts: dualCerts,
isCustomSSL
});
}
}
continue;
}
if (fqdn) { if (fqdn) {
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
const nakedDomain = domain.replace(/^www\./, ''); const nakedDomain = domain.replace(/^www\./, '');
@ -626,7 +683,8 @@ export async function remoteTraefikConfiguration(request: FastifyRequest<OnlyId>
isRunning, isRunning,
isHttps, isHttps,
isWWW, isWWW,
isDualCerts: dualCerts isDualCerts: dualCerts,
isCustomSSL
}); });
} }
if (previews) { if (previews) {
@ -649,7 +707,8 @@ export async function remoteTraefikConfiguration(request: FastifyRequest<OnlyId>
nakedDomain, nakedDomain,
isHttps, isHttps,
isWWW, isWWW,
isDualCerts: dualCerts isDualCerts: dualCerts,
isCustomSSL
}); });
} }
} }

View File

@ -48,6 +48,7 @@
"daisyui": "2.24.2", "daisyui": "2.24.2",
"dayjs": "1.11.5", "dayjs": "1.11.5",
"js-cookie": "3.0.1", "js-cookie": "3.0.1",
"js-yaml": "4.1.0",
"p-limit": "4.0.0", "p-limit": "4.0.0",
"svelte-file-dropzone": "^1.0.0", "svelte-file-dropzone": "^1.0.0",
"svelte-select": "4.4.7", "svelte-select": "4.4.7",

View File

@ -110,7 +110,7 @@ async function send({
if ( if (
response.status === 401 && response.status === 401 &&
!path.startsWith('https://api.github') && !path.startsWith('https://api.github') &&
!path.includes('/v4/user') !path.includes('/v4/')
) { ) {
Cookies.remove('token'); Cookies.remove('token');
} }

View File

@ -12,6 +12,7 @@
</script> </script>
<div class={`dropdown dropdown-end ${position}`}> <div class={`dropdown dropdown-end ${position}`}>
<!-- svelte-ignore a11y-label-has-associated-control -->
<label tabindex="0" class="btn btn-circle btn-ghost btn-xs text-sky-500"> <label tabindex="0" class="btn btn-circle btn-ghost btn-xs text-sky-500">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-4 h-4 stroke-current"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-4 h-4 stroke-current"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
</label> </label>

View File

@ -40,4 +40,6 @@
<Icons.Laravel {isAbsolute} /> <Icons.Laravel {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'heroku'} {:else if application.buildPack?.toLowerCase() === 'heroku'}
<Icons.Heroku {isAbsolute} /> <Icons.Heroku {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'compose'}
<Icons.Compose {isAbsolute} />
{/if} {/if}

View File

@ -0,0 +1,9 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<img
alt="docker compose logo"
class={isAbsolute ? 'w-16 h-16 absolute top-0 left-0 -m-8' : 'w-8 h-8 mx-auto'}
src="/docker-compose.png"
/>

View File

@ -17,3 +17,4 @@ export { default as Eleventy } from './Eleventy.svelte';
export { default as Deno } from './Deno.svelte'; export { default as Deno } from './Deno.svelte';
export { default as Laravel } from './Laravel.svelte'; export { default as Laravel } from './Laravel.svelte';
export { default as Heroku } from './Heroku.svelte'; export { default as Heroku } from './Heroku.svelte';
export { default as Compose } from './Compose.svelte';

View File

@ -56,6 +56,7 @@ export const isDeploymentEnabled: Writable<boolean> = writable(false);
export function checkIfDeploymentEnabledApplications(isAdmin: boolean, application: any) { export function checkIfDeploymentEnabledApplications(isAdmin: boolean, application: any) {
return ( return (
isAdmin && isAdmin &&
(application.buildPack === 'compose') ||
(application.fqdn || application.settings.isBot) && (application.fqdn || application.settings.isBot) &&
application.gitSource && application.gitSource &&
application.repository && application.repository &&
@ -74,9 +75,8 @@ export function checkIfDeploymentEnabledServices(isAdmin: boolean, service: any)
} }
export const status: Writable<any> = writable({ export const status: Writable<any> = writable({
application: { application: {
isRunning: false, statuses: [],
isExited: false, overallStatus: 'stopped',
isRestarting: false,
loading: false, loading: false,
initialLoading: true initialLoading: true
}, },

View File

@ -29,7 +29,7 @@ export function findBuildPack(pack: string, packageManager = 'npm') {
port: 80 port: 80
}; };
} }
if (pack === 'docker') { if (pack === 'docker' || pack === 'compose') {
return { return {
...metaData, ...metaData,
installCommand: null, installCommand: null,
@ -39,6 +39,7 @@ export function findBuildPack(pack: string, packageManager = 'npm') {
port: null port: null
}; };
} }
if (pack === 'svelte') { if (pack === 'svelte') {
return { return {
...metaData, ...metaData,
@ -235,6 +236,14 @@ export const buildPacks = [
color: 'bg-sky-700', color: 'bg-sky-700',
isCoolifyBuildPack: true, isCoolifyBuildPack: true,
}, },
{
name: 'compose',
type: 'base',
fancyName: 'Docker Compose',
hoverColor: 'hover:bg-sky-700',
color: 'bg-sky-700',
isCoolifyBuildPack: true,
},
{ {
name: 'svelte', name: 'svelte',
type: 'specific', type: 'specific',

View File

@ -18,7 +18,7 @@
<div class="dropdown dropdown-bottom"> <div class="dropdown dropdown-bottom">
<slot> <slot>
<label for="new" tabindex="0" class="btn btn-sm text-sm bg-coollabs hover:bg-coollabs-100"> <label for="new" tabindex="0" class="btn btn-sm text-sm bg-coollabs hover:bg-coollabs-100 w-52">
<svg <svg
class="h-6 w-6" class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -37,7 +37,7 @@
<ul id="new" tabindex="0" class="dropdown-content menu p-2 shadow bg-coolgray-300 rounded w-52"> <ul id="new" tabindex="0" class="dropdown-content menu p-2 shadow bg-coolgray-300 rounded w-52">
<li> <li>
<button on:click={newApplication} class="no-underline hover:bg-applications rounded-none "> <button on:click={newApplication} class="no-underline hover:bg-applications tracking-wide font-bold">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6" class="h-6 w-6"
@ -58,7 +58,7 @@
> >
</li> </li>
<li> <li>
<button on:click={newService} class="no-underline hover:bg-services rounded-none "> <button on:click={newService} class="no-underline hover:bg-services tracking-wide font-bold">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6" class="h-6 w-6"
@ -75,7 +75,7 @@
> >
</li> </li>
<li> <li>
<button on:click={newDatabase} class="no-underline hover:bg-databases rounded-none "> <button on:click={newDatabase} class="no-underline hover:bg-databases tracking-wide font-bold">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6" class="h-6 w-6"
@ -94,7 +94,7 @@
> >
</li> </li>
<li> <li>
<a href="/sources/new" class="no-underline hover:bg-sources rounded-none "> <a href="/sources/new" class="no-underline hover:bg-sources tracking-wide font-bold">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6" class="h-6 w-6"
@ -116,7 +116,7 @@
> >
</li> </li>
<li> <li>
<a href="/destinations/new" class="no-underline hover:bg-destinations rounded-none "> <a href="/destinations/new" class="no-underline hover:bg-destinations tracking-wide font-bold">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6" class="h-6 w-6"

View File

@ -160,12 +160,12 @@
<span>Logs</span> <span>Logs</span>
</li> </li>
<li <li
class:text-stone-600={!$status.application.isRunning} class:text-stone-600={$status.application.overallStatus === 'stopped'}
class="rounded" class="rounded"
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/logs`} class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/logs`}
> >
<a <a
href={$status.application.isRunning ? `/applications/${$page.params.id}/logs` : ''} href={$status.application.overallStatus !== 'stopped' ? `/applications/${$page.params.id}/logs` : ''}
class="no-underline w-full" class="no-underline w-full"
><svg ><svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -218,10 +218,10 @@
</li> </li>
<li <li
class="rounded" class="rounded"
class:text-stone-600={!$status.application.isRunning} class:text-stone-600={$status.application.overallStatus !== 'healthy'}
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/usage`} class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/usage`}
> >
<a href={$status.application.isRunning ? `/applications/${$page.params.id}/usage` : ''} class="no-underline w-full" <a href={$status.application.overallStatus === 'healthy' ? `/applications/${$page.params.id}/usage` : ''} class="no-underline w-full"
><svg ><svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6" class="w-6 h-6"

View File

@ -59,7 +59,6 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import { import {
appSession, appSession,
status, status,
@ -140,13 +139,11 @@
async function stopApplication() { async function stopApplication() {
try { try {
$status.application.initialLoading = true; $status.application.initialLoading = true;
// $status.application.loading = true;
await post(`/applications/${id}/stop`, {}); await post(`/applications/${id}/stop`, {});
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {
$status.application.initialLoading = false; $status.application.initialLoading = false;
// $status.application.loading = false;
await getStatus(); await getStatus();
} }
} }
@ -154,18 +151,48 @@
if ($status.application.loading) return; if ($status.application.loading) return;
$status.application.loading = true; $status.application.loading = true;
const data = await get(`/applications/${id}/status`); const data = await get(`/applications/${id}/status`);
$status.application.isRunning = data.isRunning;
$status.application.isExited = data.isExited; $status.application.statuses = data;
$status.application.isRestarting = data.isRestarting; let numberOfApplications = 0;
if (application.dockerComposeConfiguration) {
numberOfApplications =
application.buildPack === 'compose'
? Object.entries(JSON.parse(application.dockerComposeConfiguration)).length
: 1;
} else {
numberOfApplications = 1;
}
if ($status.application.statuses.length === 0) {
$status.application.overallStatus = 'stopped';
} else {
if ($status.application.statuses.length !== numberOfApplications) {
$status.application.overallStatus = 'degraded';
} else {
for (const oneStatus of $status.application.statuses) {
if (oneStatus.status.isExited || oneStatus.status.isRestarting) {
$status.application.overallStatus = 'degraded';
break;
}
if (oneStatus.status.isRunning) {
$status.application.overallStatus = 'healthy';
}
if (
!oneStatus.status.isExited &&
!oneStatus.status.isRestarting &&
!oneStatus.status.isRunning
) {
$status.application.overallStatus = 'stopped';
}
}
}
}
$status.application.loading = false; $status.application.loading = false;
$status.application.initialLoading = false; $status.application.initialLoading = false;
} }
onDestroy(() => { onDestroy(() => {
$status.application.initialLoading = true; $status.application.initialLoading = true;
$status.application.isRunning = false;
$status.application.isExited = false;
$status.application.isRestarting = false;
$status.application.loading = false; $status.application.loading = false;
$location = null; $location = null;
$isDeploymentEnabled = false; $isDeploymentEnabled = false;
@ -173,15 +200,8 @@
}); });
onMount(async () => { onMount(async () => {
setLocation(application, settings); setLocation(application, settings);
$status.application.isRunning = false;
$status.application.isExited = false;
$status.application.isRestarting = false;
$status.application.loading = false; $status.application.loading = false;
if ( if ($isDeploymentEnabled) {
application.gitSourceId &&
application.destinationDockerId &&
(application.fqdn || application.settings.isBot)
) {
await getStatus(); await getStatus();
statusInterval = setInterval(async () => { statusInterval = setInterval(async () => {
await getStatus(); await getStatus();
@ -207,11 +227,16 @@
<div class="flex justify-center items-center space-x-2"> <div class="flex justify-center items-center space-x-2">
<div>Configurations</div> <div>Configurations</div>
<div <div
class="badge rounded uppercase" class="badge badge-lg rounded uppercase"
class:text-green-500={$status.application.isRunning} class:text-green-500={$status.application.overallStatus === 'healthy'}
class:text-red-500={!$status.application.isRunning} class:text-yellow-400={$status.application.overallStatus === 'degraded'}
class:text-red-500={$status.application.overallStatus === 'stopped'}
> >
{$status.application.isRunning ? 'Running' : 'Stopped'} {$status.application.overallStatus === 'healthy'
? 'Running'
: $status.application.overallStatus === 'degraded'
? 'Degraded'
: 'Stopped'}
</div> </div>
</div> </div>
{/if} {/if}
@ -245,16 +270,15 @@
<div <div
class="pt-4 flex flex-row items-start justify-center lg:justify-end space-x-2 order-1 lg:order-2" class="pt-4 flex flex-row items-start justify-center lg:justify-end space-x-2 order-1 lg:order-2"
> >
{#if $status.application.isExited || $status.application.isRestarting} {#if $status.application.overallStatus === 'degraded' && application.buildPack !== 'compose'}
<a <a
id="applicationerror"
href={$isDeploymentEnabled ? `/applications/${id}/logs` : null} href={$isDeploymentEnabled ? `/applications/${id}/logs` : null}
class="icons bg-transparent text-sm text-error" class="btn btn-sm text-sm gap-2"
sveltekit:prefetch sveltekit:prefetch
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6" class="w-6 h-6 text-red-500"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentcolor" stroke="currentcolor"
@ -269,14 +293,14 @@
<line x1="12" y1="8" x2="12" y2="12" /> <line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" /> <line x1="12" y1="16" x2="12.01" y2="16" />
</svg> </svg>
Application Error
</a> </a>
<Tooltip triggeredBy="#applicationerror">Application exited with an error!</Tooltip>
{/if} {/if}
{#if $status.application.initialLoading} {#if $status.application.initialLoading}
<button class="icons animate-spin bg-transparent duration-500 ease-in-out"> <button class="btn btn-ghost btn-sm gap-2">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6" class="h-6 w-6 animate-spin duration-500 ease-in-out"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@ -292,18 +316,18 @@
<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" /> <line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
<line x1="11" y1="19.94" x2="11" y2="19.95" /> <line x1="11" y1="19.94" x2="11" y2="19.95" />
</svg> </svg>
Loading...
</button> </button>
{:else if $status.application.isRunning} {:else if $status.application.overallStatus === 'healthy'}
<button <button
id="stop"
on:click={stopApplication} on:click={stopApplication}
type="submit" type="submit"
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-error" class="btn btn-sm btn-error gap-2"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6" class="w-6 h-6 "
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@ -314,16 +338,14 @@
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="6" y="5" width="4" height="14" rx="1" /> <rect x="6" y="5" width="4" height="14" rx="1" />
<rect x="14" y="5" width="4" height="14" rx="1" /> <rect x="14" y="5" width="4" height="14" rx="1" />
</svg> </svg> Stop
</button> </button>
<Tooltip triggeredBy="#stop">Stop</Tooltip> {#if application.buildPack !== 'compose'}
<button <button
id="restart"
on:click={restartApplication} on:click={restartApplication}
type="submit" type="submit"
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled}
class="icons bg-transparent" class="btn btn-sm gap-2"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -338,14 +360,12 @@
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" /> <path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" />
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" /> <path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" />
</svg> </svg> Restart
</button> </button>
<Tooltip triggeredBy="#restart">Restart (useful to change secrets)</Tooltip> {/if}
<button <button
id="forceredeploy"
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled}
class="icons bg-transparent " class="btn btn-sm gap-2"
on:click={() => handleDeploySubmit(true)} on:click={() => handleDeploySubmit(true)}
> >
<svg <svg
@ -364,17 +384,43 @@
transform="rotate(-45 12 12)" transform="rotate(-45 12 12)"
/> />
</svg> </svg>
Force Redeploy
</button> </button>
<Tooltip triggeredBy="#forceredeploy">Force Redeploy (without cache)</Tooltip>
{:else if $isDeploymentEnabled && !$page.url.pathname.startsWith(`/applications/${id}/configuration/`)} {:else if $isDeploymentEnabled && !$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
{#if $status.application.overallStatus === 'degraded'}
<button <button
class="icons flex items-center font-bold" on:click={stopApplication}
type="submit"
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled}
on:click={() => handleDeploySubmit(false)} class="btn btn-sm btn-error gap-2"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 mr-2 text-green-500" 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> Stop
</button>
{/if}
<button
class="btn btn-sm gap-2"
class:btn-primary={$status.application.overallStatus !== 'degraded'}
disabled={!$isDeploymentEnabled}
on:click={() => handleDeploySubmit(false)}
>
{#if $status.application.overallStatus !== 'degraded'}
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@ -385,12 +431,33 @@
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" /> <path d="M7 4v16l13 -8z" />
</svg> </svg>
Deploy {:else}
<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>
{/if}
{$status.application.overallStatus === 'degraded'
? $status.application.statuses.length === 1
? 'Force Redeploy'
: 'Redeploy Stack'
: 'Deploy'}
</button> </button>
{/if} {/if}
{#if $location && $status.application.overallStatus === 'healthy'}
{#if $location && $status.application.isRunning} <a href={$location} target="_blank" class="btn btn-sm gap-2 text-sm bg-primary"
<a id="openApplication" href={$location} target="_blank" class="icons bg-transparent "
><svg ><svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6" class="h-6 w-6"
@ -405,9 +472,8 @@
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" /> <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" /> <line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" /> <polyline points="15 4 20 4 20 9" />
</svg></a </svg>Open</a
> >
<Tooltip triggeredBy="#openApplication">Open Application</Tooltip>
{/if} {/if}
</div> </div>
</div> </div>

View File

@ -14,6 +14,9 @@
export let foundConfig: any; export let foundConfig: any;
export let scanning: any; export let scanning: any;
export let packageManager: any; export let packageManager: any;
export let dockerComposeFile: string | null = null;
export let dockerComposeFileLocation: string | null = null;
export let dockerComposeConfiguration: any = null;
async function handleSubmit(name: string) { async function handleSubmit(name: string) {
try { try {
@ -25,10 +28,20 @@
delete tempBuildPack.fancyName; delete tempBuildPack.fancyName;
delete tempBuildPack.color; delete tempBuildPack.color;
delete tempBuildPack.hoverColor; delete tempBuildPack.hoverColor;
let composeConfiguration: any = {}
if (foundConfig?.buildPack !== name) { if (!dockerComposeConfiguration && dockerComposeFile) {
await post(`/applications/${id}`, { ...tempBuildPack, buildPack: name }); for (const [name, _] of Object.entries(JSON.parse(dockerComposeFile).services)) {
composeConfiguration[name] = {};
} }
}
await post(`/applications/${id}`, {
...tempBuildPack,
buildPack: name,
dockerComposeFile,
dockerComposeFileLocation,
dockerComposeConfiguration: JSON.stringify(composeConfiguration) || JSON.stringify({})
});
await post(`/applications/${id}/configuration/buildpack`, { buildPack: name }); await post(`/applications/${id}/configuration/buildpack`, { buildPack: name });
return await goto(from || `/applications/${id}`); return await goto(from || `/applications/${id}`);
} catch (error) { } catch (error) {

View File

@ -95,7 +95,7 @@
if (newWindow?.closed) { if (newWindow?.closed) {
clearInterval(timer); clearInterval(timer);
$appSession.tokens.gitlab = localStorage.getItem('gitLabToken'); $appSession.tokens.gitlab = localStorage.getItem('gitLabToken');
localStorage.removeItem('gitLabToken'); // localStorage.removeItem('gitLabToken');
resolve(); resolve();
} }
}, 100); }, 100);

View File

@ -165,7 +165,7 @@
placeholder="eg: https://github.com/coollabsio/nodejs-example/tree/main" placeholder="eg: https://github.com/coollabsio/nodejs-example/tree/main"
bind:value={publicRepositoryLink} bind:value={publicRepositoryLink}
/> />
<button class="btn bg-orange-600" class:loading={loading.branches} type="submit"> <button class="btn bg-orange-600" type="submit">
Load Repository Load Repository
</button> </button>
</div> </div>

View File

@ -12,6 +12,7 @@
const response = await get(`/applications/${params.id}/configuration/buildpack`); const response = await get(`/applications/${params.id}/configuration/buildpack`);
return { return {
props: { props: {
application,
...response ...response
} }
}; };
@ -25,22 +26,6 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { get } from '$lib/api';
import { appSession } from '$lib/store';
import { t } from '$lib/translations';
import { buildPacks, findBuildPack, scanningTemplates } from '$lib/templates';
import { errorNotification } from '$lib/common';
import BuildPack from './_BuildPack.svelte';
const { id } = $page.params;
let scanning = true;
let foundConfig: any = null;
let packageManager = 'npm';
export let apiUrl: any; export let apiUrl: any;
export let projectId: any; export let projectId: any;
export let repository: any; export let repository: any;
@ -49,6 +34,28 @@
export let application: any; export let application: any;
export let isPublicRepository: boolean; export let isPublicRepository: boolean;
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { get, getAPIUrl } from '$lib/api';
import { appSession } from '$lib/store';
import { t } from '$lib/translations';
import { buildPacks, findBuildPack, scanningTemplates } from '$lib/templates';
import { errorNotification } from '$lib/common';
import BuildPack from './_BuildPack.svelte';
import yaml from 'js-yaml';
const { id } = $page.params;
let htmlUrl = application.gitSource.htmlUrl;
let scanning: boolean = true;
let foundConfig: any = null;
let packageManager: string = 'npm';
let dockerComposeFile: string | null = application.dockerComposeFile || null;
let dockerComposeFileLocation: string | null = application.dockerComposeFileLocation || null;
let dockerComposeConfiguration: any = application.dockerComposeConfiguration || null;
function checkPackageJSONContents({ key, json }: { key: any; json: any }) { function checkPackageJSONContents({ key, json }: { key: any; json: any }) {
return json?.dependencies?.hasOwnProperty(key) || json?.devDependencies?.hasOwnProperty(key); return json?.dependencies?.hasOwnProperty(key) || json?.devDependencies?.hasOwnProperty(key);
} }
@ -60,11 +67,45 @@
} }
} }
} }
async function scanRepository(): Promise<void> { async function getGitlabToken() {
return await new Promise<void>((resolve, reject) => {
const left = screen.width / 2 - 1020 / 2;
const top = screen.height / 2 - 618 / 2;
const newWindow = open(
`${htmlUrl}/oauth/authorize?client_id=${
application.gitSource.gitlabApp.appId
}&redirect_uri=${getAPIUrl()}/webhooks/gitlab&response_type=code&scope=api+email+read_repository&state=${
$page.params.id
}`,
'GitLab',
'resizable=1, scrollbars=1, fullscreen=0, height=618, width=1020,top=' +
top +
', left=' +
left +
', toolbar=0, menubar=0, status=0'
);
const timer = setInterval(() => {
if (newWindow?.closed) {
clearInterval(timer);
$appSession.tokens.gitlab = localStorage.getItem('gitLabToken');
localStorage.removeItem('gitLabToken');
resolve();
}
}, 100);
});
}
async function scanRepository(isPublicRepository: boolean): Promise<void> {
try { try {
if (type === 'gitlab') { if (type === 'gitlab') {
const files = await get(`${apiUrl}/v4/projects/${projectId}/repository/tree`, { const headers = isPublicRepository
? {}
: {
Authorization: `Bearer ${$appSession.tokens.gitlab}` Authorization: `Bearer ${$appSession.tokens.gitlab}`
};
const url = isPublicRepository ? `/projects/${projectId}/repository/tree` : `/v4/projects/${projectId}/repository/tree`;
const files = await get(`${apiUrl}${url}`, {
...headers
}); });
const packageJson = files.find( const packageJson = files.find(
(file: { name: string; type: string }) => (file: { name: string; type: string }) =>
@ -82,6 +123,14 @@
(file: { name: string; type: string }) => (file: { name: string; type: string }) =>
file.name === 'Dockerfile' && file.type === 'blob' file.name === 'Dockerfile' && file.type === 'blob'
); );
const dockerComposeFileYml = files.find(
(file: { name: string; type: string }) =>
file.name === 'docker-compose.yml' && file.type === 'blob'
);
const dockerComposeFileYaml = files.find(
(file: { name: string; type: string }) =>
file.name === 'docker-compose.yaml' && file.type === 'blob'
);
const cargoToml = files.find( const cargoToml = files.find(
(file: { name: string; type: string }) => (file: { name: string; type: string }) =>
file.name === 'Cargo.toml' && file.type === 'blob' file.name === 'Cargo.toml' && file.type === 'blob'
@ -105,11 +154,24 @@
const laravel = files.find( const laravel = files.find(
(file: { name: string; type: string }) => file.name === 'artisan' && file.type === 'blob' (file: { name: string; type: string }) => file.name === 'artisan' && file.type === 'blob'
); );
if (yarnLock) packageManager = 'yarn'; if (yarnLock) packageManager = 'yarn';
if (pnpmLock) packageManager = 'pnpm'; if (pnpmLock) packageManager = 'pnpm';
if (dockerfile) { if (dockerComposeFileYml || dockerComposeFileYaml) {
foundConfig = findBuildPack('compose', packageManager);
const id = dockerComposeFileYml.id || dockerComposeFileYaml.id;
const data = await get(`${apiUrl}/v4/projects/${projectId}/repository/blobs/${id}`, {
...headers
});
if (data?.content) {
const content = atob(data.content);
const dockerComposeJson = yaml.load(content) || null;
dockerComposeFile = JSON.stringify(dockerComposeJson);
dockerComposeFileLocation = dockerComposeFileYml
? 'docker-compose.yml'
: 'docker-compose.yaml';
}
} else if (dockerfile) {
foundConfig = findBuildPack('docker', packageManager); foundConfig = findBuildPack('docker', packageManager);
} else if (packageJson && !laravel) { } else if (packageJson && !laravel) {
const path = packageJson.path; const path = packageJson.path;
@ -135,8 +197,13 @@
foundConfig = findBuildPack('node', packageManager); foundConfig = findBuildPack('node', packageManager);
} }
} else if (type === 'github') { } else if (type === 'github') {
const headers = isPublicRepository
? {}
: {
Authorization: `token ${$appSession.tokens.github}`
};
const files = await get(`${apiUrl}/repos/${repository}/contents?ref=${branch}`, { const files = await get(`${apiUrl}/repos/${repository}/contents?ref=${branch}`, {
Authorization: `Bearer ${$appSession.tokens.github}`, ...headers,
Accept: 'application/vnd.github.v2.json' Accept: 'application/vnd.github.v2.json'
}); });
const packageJson = files.find( const packageJson = files.find(
@ -155,6 +222,14 @@
(file: { name: string; type: string }) => (file: { name: string; type: string }) =>
file.name === 'Dockerfile' && file.type === 'file' file.name === 'Dockerfile' && file.type === 'file'
); );
const dockerComposeFileYml = files.find(
(file: { name: string; type: string }) =>
file.name === 'docker-compose.yml' && file.type === 'file'
);
const dockerComposeFileYaml = files.find(
(file: { name: string; type: string }) =>
file.name === 'docker-compose.yaml' && file.type === 'file'
);
const cargoToml = files.find( const cargoToml = files.find(
(file: { name: string; type: string }) => (file: { name: string; type: string }) =>
file.name === 'Cargo.toml' && file.type === 'file' file.name === 'Cargo.toml' && file.type === 'file'
@ -182,11 +257,30 @@
if (yarnLock) packageManager = 'yarn'; if (yarnLock) packageManager = 'yarn';
if (pnpmLock) packageManager = 'pnpm'; if (pnpmLock) packageManager = 'pnpm';
if (dockerfile) { if (dockerComposeFileYml || dockerComposeFileYaml) {
foundConfig = findBuildPack('compose', packageManager);
const data = await get(
`${apiUrl}/repos/${repository}/contents/${
dockerComposeFileYml ? 'docker-compose.yml' : 'docker-compose.yaml'
}?ref=${branch}`,
{
...headers,
Accept: 'application/vnd.github.v2.json'
}
);
if (data?.content) {
const content = atob(data.content);
const dockerComposeJson = yaml.load(content) || null;
dockerComposeFile = JSON.stringify(dockerComposeJson);
dockerComposeFileLocation = dockerComposeFileYml
? 'docker-compose.yml'
: 'docker-compose.yaml';
}
} else if (dockerfile) {
foundConfig = findBuildPack('docker', packageManager); foundConfig = findBuildPack('docker', packageManager);
} else if (packageJson && !laravel) { } else if (packageJson && !laravel) {
const data: any = await get(`${packageJson.git_url}`, { const data: any = await get(`${packageJson.git_url}`, {
Authorization: `Bearer ${$appSession.tokens.github}`, ...headers,
Accept: 'application/vnd.github.v2.raw' Accept: 'application/vnd.github.v2.raw'
}); });
const json = JSON.parse(data) || {}; const json = JSON.parse(data) || {};
@ -214,30 +308,39 @@
error.message === '401 Unauthorized' error.message === '401 Unauthorized'
) { ) {
if (application.gitSource.gitlabAppId) { if (application.gitSource.gitlabAppId) {
let htmlUrl = application.gitSource.htmlUrl; if (!$appSession.tokens.gitlab) {
const left = screen.width / 2 - 1020 / 2; await getGitlabToken();
const top = screen.height / 2 - 618 / 2;
const newWindow = open(
`${htmlUrl}/oauth/authorize?client_id=${application.gitSource.gitlabApp.appId}&redirect_uri=${window.location.origin}/webhooks/gitlab&response_type=code&scope=api+email+read_repository&state=${$page.params.id}`,
'GitLab',
'resizable=1, scrollbars=1, fullscreen=0, height=618, width=1020,top=' +
top +
', left=' +
left +
', toolbar=0, menubar=0, status=0'
);
const timer = setInterval(() => {
if (newWindow?.closed) {
clearInterval(timer);
window.location.reload();
} }
}, 100); scanRepository(isPublicRepository);
// let htmlUrl = application.gitSource.htmlUrl;
// const left = screen.width / 2 - 1020 / 2;
// const top = screen.height / 2 - 618 / 2;
// const newWindow = open(
// `${htmlUrl}/oauth/authorize?client_id=${
// application.gitSource.gitlabApp.appId
// }&redirect_uri=${getAPIUrl()}/webhooks/gitlab&response_type=code&scope=api+email+read_repository&state=${
// $page.params.id
// }`,
// 'GitLab',
// 'resizable=1, scrollbars=1, fullscreen=0, height=618, width=1020,top=' +
// top +
// ', left=' +
// left +
// ', toolbar=0, menubar=0, status=0'
// );
// const timer = setInterval(() => {
// if (newWindow?.closed) {
// clearInterval(timer);
// $appSession.tokens.gitlab = localStorage.getItem('gitLabToken');
// // localStorage.removeItem('gitLabToken' );
// }
// }, 100);
} }
} } else if (error.message === 'Bad credentials') {
if (error.message === 'Bad credentials') {
const { token } = await get(`/applications/${id}/configuration/githubToken`); const { token } = await get(`/applications/${id}/configuration/githubToken`);
$appSession.tokens.github = token; $appSession.tokens.github = token;
return await scanRepository(); return await scanRepository(isPublicRepository);
} }
return errorNotification(error); return errorNotification(error);
} finally { } finally {
@ -246,11 +349,7 @@
} }
} }
onMount(async () => { onMount(async () => {
if (!isPublicRepository) { await scanRepository(isPublicRepository);
await scanRepository();
} else {
scanning = false;
}
}); });
</script> </script>
@ -274,9 +373,17 @@
<div class="max-w-screen-2xl mx-auto px-10"> <div class="max-w-screen-2xl mx-auto px-10">
<div class="title pb-2">Coolify Base</div> <div class="title pb-2">Coolify Base</div>
<div class="flex flex-wrap justify-center"> <div class="flex flex-wrap justify-center">
{#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type ==='base') as buildPack} {#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type === 'base') as buildPack}
<div class="p-2"> <div class="p-2">
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig /> <BuildPack
{packageManager}
{buildPack}
{scanning}
bind:foundConfig
{dockerComposeFile}
{dockerComposeFileLocation}
{dockerComposeConfiguration}
/>
</div> </div>
{/each} {/each}
</div> </div>
@ -284,7 +391,7 @@
<div class="max-w-screen-2xl mx-auto px-10"> <div class="max-w-screen-2xl mx-auto px-10">
<div class="title pb-2">Coolify Specific</div> <div class="title pb-2">Coolify Specific</div>
<div class="flex flex-wrap justify-center"> <div class="flex flex-wrap justify-center">
{#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type ==='specific') as buildPack} {#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type === 'specific') as buildPack}
<div class="p-2"> <div class="p-2">
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig /> <BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
</div> </div>

View File

@ -28,10 +28,12 @@
<script lang="ts"> <script lang="ts">
export let application: any; export let application: any;
export let settings: any; export let settings: any;
import yaml from 'js-yaml';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { onDestroy, onMount } from 'svelte'; import { onMount } from 'svelte';
import Select from 'svelte-select'; import Select from 'svelte-select';
import { get, post } from '$lib/api'; import { get, getAPIUrl, post } from '$lib/api';
import cuid from 'cuid'; import cuid from 'cuid';
import { import {
addToast, addToast,
@ -45,19 +47,27 @@
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common'; import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common';
import Setting from '$lib/components/Setting.svelte'; import Setting from '$lib/components/Setting.svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
import Explainer from '$lib/components/Explainer.svelte'; import Explainer from '$lib/components/Explainer.svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { fade } from 'svelte/transition'; import Beta from '$lib/components/Beta.svelte';
const { id } = $page.params; const { id } = $page.params;
$: isDisabled = $: isDisabled =
!$appSession.isAdmin || $status.application.isRunning || $status.application.initialLoading; !$appSession.isAdmin ||
$status.application.overallStatus === 'degraded' ||
$status.application.overallStatus === 'healthy' ||
$status.application.initialLoading;
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
let statues: any = {};
let loading = false; let loading = false;
let fqdnEl: any = null; let fqdnEl: any = null;
let forceSave = false; let forceSave = false;
let isPublicRepository = application.settings.isPublicRepository;
let apiUrl = application.gitSource.apiUrl;
let branch = application.branch;
let repository = application.repository;
let debug = application.settings.debug; let debug = application.settings.debug;
let previews = application.settings.previews; let previews = application.settings.previews;
let dualCerts = application.settings.dualCerts; let dualCerts = application.settings.dualCerts;
@ -65,6 +75,12 @@
let autodeploy = application.settings.autodeploy; let autodeploy = application.settings.autodeploy;
let isBot = application.settings.isBot; let isBot = application.settings.isBot;
let isDBBranching = application.settings.isDBBranching; let isDBBranching = application.settings.isDBBranching;
let htmlUrl = application.gitSource.htmlUrl;
let dockerComposeFile = JSON.parse(application.dockerComposeFile) || null;
let dockerComposeServices: any[] = [];
let dockerComposeFileLocation = application.dockerComposeFileLocation;
let dockerComposeConfiguration = JSON.parse(application.dockerComposeConfiguration) || {};
let baseDatabaseBranch: any = application?.connectedDatabase?.hostedDatabaseDBName || null; let baseDatabaseBranch: any = application?.connectedDatabase?.hostedDatabaseDBName || null;
let nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, ''); let nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
@ -86,6 +102,26 @@
label: 'Uvicorn' label: 'Uvicorn'
} }
]; ];
function normalizeDockerServices(services: any[]) {
const tempdockerComposeServices = [];
for (const [name, data] of Object.entries(services)) {
tempdockerComposeServices.push({
name,
data
});
}
for (const service of tempdockerComposeServices) {
if (!dockerComposeConfiguration[service.name]) {
dockerComposeConfiguration[service.name] = {};
}
}
return tempdockerComposeServices;
}
if (dockerComposeFile?.services) {
dockerComposeServices = normalizeDockerServices(dockerComposeFile.services);
}
function containerClass() { function containerClass() {
return 'text-white bg-transparent font-thin px-0 w-full border border-dashed border-coolgray-200'; return 'text-white bg-transparent font-thin px-0 w-full border border-dashed border-coolgray-200';
} }
@ -148,7 +184,7 @@
isCustomSSL = !isCustomSSL; isCustomSSL = !isCustomSSL;
} }
if (name === 'isBot') { if (name === 'isBot') {
if ($status.application.isRunning) return; if ($status.application.overallStatus !== 'stopped') return;
isBot = !isBot; isBot = !isBot;
application.settings.isBot = isBot; application.settings.isBot = isBot;
application.fqdn = null; application.fqdn = null;
@ -200,26 +236,42 @@
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application); $isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
} }
} }
async function handleSubmit() { async function handleSubmit(toast: boolean = true) {
if (loading) return; if (loading) return;
loading = true; if (toast) loading = true;
try { try {
nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, ''); nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
if (application.deploymentType) if (application.deploymentType)
application.deploymentType = application.deploymentType.toLowerCase(); application.deploymentType = application.deploymentType.toLowerCase();
!isBot && if (!isBot) {
(await post(`/applications/${id}/check`, { await post(`/applications/${id}/check`, {
fqdn: application.fqdn, fqdn: application.fqdn,
forceSave, forceSave,
dualCerts, dualCerts,
exposePort: application.exposePort exposePort: application.exposePort
})); });
await post(`/applications/${id}`, { ...application, baseDatabaseBranch }); for (const service of dockerComposeServices) {
if (dockerComposeConfiguration[service.name].fqdn) {
await post(`/applications/${id}/check`, {
fqdn: dockerComposeConfiguration[service.name].fqdn,
forceSave,
dualCerts,
exposePort: application.exposePort
});
}
}
}
await post(`/applications/${id}`, {
...application,
baseDatabaseBranch,
dockerComposeConfiguration: JSON.stringify(dockerComposeConfiguration)
});
setLocation(application, settings); setLocation(application, settings);
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application); $isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
forceSave = false; forceSave = false;
toast &&
addToast({ addToast({
message: 'Configuration saved.', message: 'Configuration saved.',
type: 'success' type: 'success'
@ -281,10 +333,130 @@
return false; return false;
} }
} }
async function getGitlabToken() {
return await new Promise<void>((resolve, reject) => {
const left = screen.width / 2 - 1020 / 2;
const top = screen.height / 2 - 618 / 2;
const newWindow = open(
`${htmlUrl}/oauth/authorize?client_id=${
application.gitSource.gitlabApp.appId
}&redirect_uri=${getAPIUrl()}/webhooks/gitlab&response_type=code&scope=api+email+read_repository&state=${
$page.params.id
}`,
'GitLab',
'resizable=1, scrollbars=1, fullscreen=0, height=618, width=1020,top=' +
top +
', left=' +
left +
', toolbar=0, menubar=0, status=0'
);
const timer = setInterval(() => {
if (newWindow?.closed) {
clearInterval(timer);
$appSession.tokens.gitlab = localStorage.getItem('gitLabToken');
localStorage.removeItem('gitLabToken');
resolve();
}
}, 100);
});
}
async function reloadCompose() {
try {
if (application.gitSource.type === 'github') {
const headers = isPublicRepository
? {}
: {
Authorization: `token ${$appSession.tokens.github}`
};
const data = await get(
`${apiUrl}/repos/${repository}/contents/${dockerComposeFileLocation}?ref=${branch}`,
{
...headers,
Accept: 'application/vnd.github.v2.json'
}
);
if (data?.content) {
const content = atob(data.content);
let dockerComposeFileContent = JSON.stringify(yaml.load(content) || null);
let dockerComposeFileContentJSON = JSON.parse(dockerComposeFileContent);
dockerComposeServices = normalizeDockerServices(dockerComposeFileContentJSON?.services);
application.dockerComposeFile = dockerComposeFileContent;
await handleSubmit(false);
}
}
if (application.gitSource.type === 'gitlab') {
if (!$appSession.tokens.gitlab) {
await getGitlabToken();
}
const headers = isPublicRepository
? {}
: {
Authorization: `Bearer ${$appSession.tokens.gitlab}`
};
const url = isPublicRepository
? ``
: `/v4/projects/${application.projectId}/repository/tree`;
const files = await get(`${apiUrl}${url}`, {
...headers
});
const dockerComposeFileYml = files.find(
(file: { name: string; type: string }) =>
file.name === dockerComposeFileLocation && file.type === 'blob'
);
const id = dockerComposeFileYml.id;
const data = await get(
`${apiUrl}/v4/projects/${application.projectId}/repository/blobs/${id}`,
{
...headers
}
);
if (data?.content) {
const content = atob(data.content);
let dockerComposeFileContent = JSON.stringify(yaml.load(content) || null);
let dockerComposeFileContentJSON = JSON.parse(dockerComposeFileContent);
dockerComposeServices = normalizeDockerServices(dockerComposeFileContentJSON?.services);
application.dockerComposeFile = dockerComposeFileContent;
await handleSubmit(false);
}
}
addToast({
message: 'Compose file reloaded.',
type: 'success'
});
} catch (error) {
errorNotification(error);
}
}
$: if ($status.application.statuses) {
for (const service of dockerComposeServices) {
getStatus(service);
}
}
function getStatus(service: any) {
let foundStatus = null;
const foundService = $status.application.statuses.find(
(s: any) => s.name === `${application.id}-${service.name}`
);
if (foundService) {
const statusText = foundService?.status;
if (statusText?.isRunning) {
foundStatus = 'Running';
}
if (statusText?.isExited) {
foundStatus = 'Exited';
}
if (statusText?.isRestarting) {
foundStatus = 'Restarting';
}
}
statues[service.name] = foundStatus || 'Stopped';
}
</script> </script>
<div class="w-full"> <div class="w-full">
<form on:submit|preventDefault={handleSubmit}> <form on:submit|preventDefault={() => handleSubmit()}>
<div class="mx-auto w-full"> <div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2"> <div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3 ">General</div> <div class="title font-bold pb-3 ">General</div>
@ -345,9 +517,9 @@
{/if} {/if}
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="buildPack">{$t('application.build_pack')}</label> <label for="buildPack">{$t('application.build_pack')} </label>
{#if isDisabled} {#if isDisabled}
<input class="uppercase w-full" disabled={isDisabled} value={application.buildPack} /> <input class="capitalize w-full" disabled={isDisabled} value={application.buildPack} />
{:else} {:else}
<a <a
href={`/applications/${id}/configuration/buildpack?from=/applications/${id}`} href={`/applications/${id}/configuration/buildpack?from=/applications/${id}`}
@ -372,6 +544,7 @@
/> />
</div> </div>
</div> </div>
{#if application.buildPack !== 'compose'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
id="isBot" id="isBot"
@ -380,10 +553,11 @@
on:click={() => changeSettings('isBot')} on:click={() => changeSettings('isBot')}
title="Is your application a bot?" title="Is your application a bot?"
description="You can deploy applications without domains or make them to listen on the <span class='text-settings font-bold'>Exposed Port</span>.<br></Setting><br>Useful to host <span class='text-settings font-bold'>Twitch bots, regular jobs, or anything that does not require an incoming HTTP connection.</span>" description="You can deploy applications without domains or make them to listen on the <span class='text-settings font-bold'>Exposed Port</span>.<br></Setting><br>Useful to host <span class='text-settings font-bold'>Twitch bots, regular jobs, or anything that does not require an incoming HTTP connection.</span>"
disabled={$status.application.isRunning} disabled={isDisabled}
/> />
</div> </div>
{#if !isBot} {/if}
{#if !isBot && application.buildPack !== 'compose'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="fqdn" <label for="fqdn"
>{$t('application.url_fqdn')} >{$t('application.url_fqdn')}
@ -446,15 +620,15 @@
<Setting <Setting
id="dualCerts" id="dualCerts"
dataTooltip={$t('forms.must_be_stopped_to_modify')} dataTooltip={$t('forms.must_be_stopped_to_modify')}
disabled={$status.application.isRunning} disabled={isDisabled}
isCenter={false} isCenter={false}
bind:setting={dualCerts} bind:setting={dualCerts}
title={$t('application.ssl_www_and_non_www')} title={$t('application.ssl_www_and_non_www')}
description="Generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-settings'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both." description="Generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-settings'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both."
on:click={() => !$status.application.isRunning && changeSettings('dualCerts')} on:click={() => !isDisabled && changeSettings('dualCerts')}
/> />
</div> </div>
{#if isHttps} {#if isHttps && application.buildPack !== 'compose'}
<div class="grid grid-cols-2 items-center pb-4"> <div class="grid grid-cols-2 items-center pb-4">
<Setting <Setting
id="isCustomSSL" id="isCustomSSL"
@ -468,8 +642,10 @@
{/if} {/if}
{/if} {/if}
</div> </div>
{#if application.buildPack !== 'compose'}
<div class="title font-bold pb-3 pt-10 border-b border-coolgray-500 mb-6">Build & Deploy</div> <div class="title font-bold pb-3 pt-10 border-b border-coolgray-500 mb-6">
Build & Deploy
</div>
<div class="grid grid-flow-row gap-2 px-4 pr-5"> <div class="grid grid-flow-row gap-2 px-4 pr-5">
{#if application.buildCommand || application.buildPack === 'rust' || application.buildPack === 'laravel'} {#if application.buildCommand || application.buildPack === 'rust' || application.buildPack === 'laravel'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
@ -486,7 +662,7 @@
{isDisabled} {isDisabled}
containerClasses={isDisabled && containerClass()} containerClasses={isDisabled && containerClass()}
id="baseBuildImages" id="baseBuildImages"
showIndicator={!$status.application.isRunning} showIndicator={!isDisabled}
items={application.baseBuildImages} items={application.baseBuildImages}
on:select={selectBaseBuildImage} on:select={selectBaseBuildImage}
value={application.baseBuildImage} value={application.baseBuildImage}
@ -506,7 +682,7 @@
{isDisabled} {isDisabled}
containerClasses={isDisabled && containerClass()} containerClasses={isDisabled && containerClass()}
id="baseImages" id="baseImages"
showIndicator={!$status.application.isRunning} showIndicator={!isDisabled}
items={application.baseImages} items={application.baseImages}
on:select={selectBaseImage} on:select={selectBaseImage}
value={application.baseImage} value={application.baseImage}
@ -528,7 +704,7 @@
{isDisabled} {isDisabled}
containerClasses={isDisabled && containerClass()} containerClasses={isDisabled && containerClass()}
id="deploymentTypes" id="deploymentTypes"
showIndicator={!$status.application.isRunning} showIndicator={!isDisabled}
items={['static', 'node']} items={['static', 'node']}
on:select={selectDeploymentType} on:select={selectDeploymentType}
value={application.deploymentType} value={application.deploymentType}
@ -639,7 +815,9 @@
<div class="grid grid-cols-2 items-center pt-4"> <div class="grid grid-cols-2 items-center pt-4">
<label for="port" <label for="port"
>{$t('forms.port')} >{$t('forms.port')}
<Explainer explanation={'The port your application listens on.'} /></label <Explainer
explanation={'The port your application listens inside the docker container.'}
/></label
> >
<input <input
class="w-full" class="w-full"
@ -660,7 +838,7 @@
> >
<input <input
class="w-full" class="w-full"
readonly={!$appSession.isAdmin && !$status.application.isRunning} readonly={!isDisabled}
disabled={isDisabled} disabled={isDisabled}
name="exposePort" name="exposePort"
id="exposePort" id="exposePort"
@ -811,6 +989,98 @@
</div> </div>
{/if} {/if}
</div> </div>
{:else}
<div class="title font-bold pb-3 pt-10 border-b border-coolgray-500 mb-6">
Stack <Beta />
{#if $appSession.isAdmin}
<button class="btn btn-sm btn-primary" on:click|preventDefault={reloadCompose}
>Reload Docker Compose File</button
>
{/if}
</div>
<div class="grid grid-flow-row gap-2">
{#each dockerComposeServices as service}
<div
class="grid items-center bg-coolgray-100 rounded border border-coolgray-300 p-2 px-4"
>
<div class="text-xl font-bold uppercase">
{service.name}
<span
class="badge rounded text-white"
class:text-red-500={statues[service.name] === 'Exited' ||
statues[service.name] === 'Stopped'}
class:text-yellow-400={statues[service.name] === 'Restarting'}
class:text-green-500={statues[service.name] === 'Running'}
>{statues[service.name] || 'Loading...'}</span
>
</div>
<div class="text-xs">{application.id}-{service.name}</div>
</div>
<div class="grid grid-cols-2 items-center px-8">
<label for="fqdn"
>{$t('application.url_fqdn')}
<Explainer
explanation={"If you specify <span class='text-settings font-bold'>https</span>, the application will be accessible only over https.<br>SSL certificate will be generated automatically.<br><br>If you specify <span class='text-settings font-bold'>www</span>, the application will be redirected (302) from non-www and vice versa.<br><br>To modify the domain, you must first stop the application.<br><br><span class='text-settings font-bold'>You must set your DNS to point to the server IP in advance.</span>"}
/>
</label>
<div>
<input
class="w-full"
disabled={isDisabled}
readonly={!$appSession.isAdmin}
name="fqdn"
id="fqdn"
bind:value={dockerComposeConfiguration[service.name].fqdn}
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="eg: https://coollabs.io"
/>
</div>
</div>
<div class="grid grid-cols-2 items-center px-8">
<label for="destinationdns"
>Internal DNS on the deployed Destination
<Explainer
explanation={'You can use these DNS names to access the application from other resources in your Destination.'}
/>
</label>
<input
for="destinationdns"
class="w-full"
disabled
readonly
value={`${application.id}-${service.name}`}
/>
</div>
<div class="grid grid-cols-2 items-center px-8">
<label for="stackdns"
>Internal DNS in the current stack
<Explainer
explanation={'You can use these DNS names to access the application from this stack.'}
/>
</label>
<input for="stackdns" class="w-full" disabled readonly value={service.name} />
</div>
<div class="grid grid-cols-2 items-center px-8 pb-4">
<label for="port"
>{$t('forms.port')}
<Explainer
explanation={'The port your application listens inside the docker container.'}
/></label
>
<input
class="w-full"
disabled={isDisabled}
readonly={!$appSession.isAdmin}
name="port"
id="port"
required={!!dockerComposeConfiguration[service.name].fqdn}
bind:value={dockerComposeConfiguration[service.name].port}
/>
</div>
{/each}
</div>
{/if}
</div> </div>
</form> </form>
</div> </div>

View File

@ -3,11 +3,9 @@
import { get } from '$lib/api'; import { get } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import LoadingLogs from '$lib/components/LoadingLogs.svelte';
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import Tooltip from '$lib/components/Tooltip.svelte'; import Tooltip from '$lib/components/Tooltip.svelte';
import { status } from '$lib/store';
import { goto } from '$app/navigation';
let application: any = {}; let application: any = {};
let logsLoading = false; let logsLoading = false;
let loadLogsInterval: any = null; let loadLogsInterval: any = null;
@ -17,47 +15,52 @@
let followingLogs: any; let followingLogs: any;
let logsEl: any; let logsEl: any;
let position = 0; let position = 0;
if ( let services: any = [];
!$status.application.isExited && let selectedService: any = null;
!$status.application.isRestarting && let noContainer = false;
!$status.application.isRunning
) {
goto(`/applications/${$page.params.id}/`, { replaceState: true });
}
const { id } = $page.params; const { id } = $page.params;
onMount(async () => { onMount(async () => {
const response = await get(`/applications/${id}`); const response = await get(`/applications/${id}`);
application = response.application; application = response.application;
loadAllLogs(); if (response.application.dockerComposeFile) {
loadLogsInterval = setInterval(() => { services = normalizeDockerServices(
loadLogs(); JSON.parse(response.application.dockerComposeFile).services
}, 1000); );
} else {
services = [
{
name: ''
}
];
await selectService('');
}
}); });
onDestroy(() => { onDestroy(() => {
clearInterval(loadLogsInterval); clearInterval(loadLogsInterval);
clearInterval(followingInterval); clearInterval(followingInterval);
}); });
async function loadAllLogs() { function normalizeDockerServices(services: any[]) {
try { const tempdockerComposeServices = [];
logsLoading = true; for (const [name, data] of Object.entries(services)) {
const data: any = await get(`/applications/${id}/logs`); tempdockerComposeServices.push({
if (data?.logs) { name,
lastLog = data.logs[data.logs.length - 1]; data
logs = data.logs; });
}
} catch (error) {
return errorNotification(error);
} finally {
logsLoading = false;
} }
return tempdockerComposeServices;
} }
async function loadLogs() { async function loadLogs() {
if (logsLoading) return; if (logsLoading) return;
try { try {
const newLogs: any = await get( const newLogs: any = await get(
`/applications/${id}/logs?since=${lastLog?.split(' ')[0] || 0}` `/applications/${id}/logs/${selectedService}?since=${lastLog?.split(' ')[0] || 0}`
); );
if (newLogs.noContainer) {
noContainer = true;
} else {
noContainer = false;
}
if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) { if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) {
logs = logs.concat(newLogs.logs); logs = logs.concat(newLogs.logs);
lastLog = newLogs.logs[newLogs.logs.length - 1]; lastLog = newLogs.logs[newLogs.logs.length - 1];
@ -89,6 +92,22 @@
clearInterval(followingInterval); clearInterval(followingInterval);
} }
} }
async function selectService(service: any, init: boolean = false) {
if (services.length === 1 && init) return;
if (loadLogsInterval) clearInterval(loadLogsInterval);
if (followingInterval) clearInterval(followingInterval);
logs = [];
lastLog = null;
followingLogs = false;
selectedService = `${application.id}${service.name ? `-${service.name}` : ''}`;
loadLogs();
loadLogsInterval = setInterval(() => {
loadLogs();
}, 1000);
}
</script> </script>
<div class="mx-auto w-full"> <div class="mx-auto w-full">
@ -96,9 +115,27 @@
<div class="title font-bold pb-3">Application Logs</div> <div class="title font-bold pb-3">Application Logs</div>
</div> </div>
</div> </div>
<div class="flex flex-row justify-center space-x-2"> <div class="flex gap-2 lg:gap-8 pb-4">
{#each services as service}
<button
on:click={() => selectService(service, true)}
class:bg-primary={selectedService ===
`${application.id}${service.name ? `-${service.name}` : ''}`}
class:bg-coolgray-200={selectedService !==
`${application.id}${service.name ? `-${service.name}` : ''}`}
class="w-full rounded p-5 hover:bg-primary font-bold"
>
{application.id}{service.name ? `-${service.name}` : ''}</button
>
{/each}
</div>
{#if selectedService}
<div class="flex flex-row justify-center space-x-2">
{#if logs.length === 0} {#if logs.length === 0}
<div class="text-xl font-bold tracking-tighter">{$t('application.build.waiting_logs')}</div> {#if noContainer}
<div class="text-xl font-bold tracking-tighter">Container not found / exited.</div>
{/if}
{:else} {:else}
<div class="relative w-full"> <div class="relative w-full">
<div class="flex justify-start sticky space-x-2 pb-2"> <div class="flex justify-start sticky space-x-2 pb-2">
@ -142,4 +179,5 @@
</div> </div>
</div> </div>
{/if} {/if}
</div> </div>
{/if}

View File

@ -1,11 +1,14 @@
<script lang="ts"> <script lang="ts">
export let application: any;
import { page } from '$app/stores'; import { page } from '$app/stores';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { get } from '$lib/api'; import { get } from '$lib/api';
import { status } from '$lib/store'; import { status } from '$lib/store';
import Tooltip from '$lib/components/Tooltip.svelte';
const { id } = $page.params; const { id } = $page.params;
let services: any = [];
let selectedService: any = null;
let usageLoading = false; let usageLoading = false;
let usage = { let usage = {
MemUsage: 0, MemUsage: 0,
@ -16,20 +19,55 @@
async function getUsage() { async function getUsage() {
if (usageLoading) return; if (usageLoading) return;
if (!$status.application.isRunning) return;
usageLoading = true; usageLoading = true;
const data = await get(`/applications/${id}/usage`); const data = await get(`/applications/${id}/usage/${selectedService}`);
usage = data.usage; usage = data.usage;
usageLoading = false; usageLoading = false;
} }
function normalizeDockerServices(services: any[]) {
const tempdockerComposeServices = [];
for (const [name, data] of Object.entries(services)) {
tempdockerComposeServices.push({
name,
data
});
}
return tempdockerComposeServices;
}
async function selectService(service: any, init: boolean = false) {
if (services.length === 1 && init) return;
if (usageInterval) clearInterval(usageInterval);
usageLoading = false;
usage = {
MemUsage: 0,
CPUPerc: 0,
NetIO: 0
};
selectedService = `${application.id}${service.name ? `-${service.name}` : ''}`;
await getUsage();
usageInterval = setInterval(async () => {
await getUsage();
}, 1000);
}
onDestroy(() => { onDestroy(() => {
clearInterval(usageInterval); clearInterval(usageInterval);
}); });
onMount(async () => { onMount(async () => {
await getUsage(); const response = await get(`/applications/${id}`);
usageInterval = setInterval(async () => { application = response.application;
await getUsage(); if (response.application.dockerComposeFile) {
}, 1000); services = normalizeDockerServices(
JSON.parse(response.application.dockerComposeFile).services
);
} else {
services = [
{
name: ''
}
];
await selectService('');
}
}); });
</script> </script>
@ -38,7 +76,29 @@
<div class="title font-bold pb-3">Monitoring</div> <div class="title font-bold pb-3">Monitoring</div>
</div> </div>
</div> </div>
<div class="mx-auto max-w-4xl px-6 py-4"> <div class="flex gap-2 lg:gap-8 pb-4">
{#each services as service}
<button
on:click={() => selectService(service, true)}
class:bg-primary={selectedService ===
`${application.id}${service.name ? `-${service.name}` : ''}`}
class:bg-coolgray-200={selectedService !==
`${application.id}${service.name ? `-${service.name}` : ''}`}
class="w-full rounded p-5 hover:bg-primary font-bold"
>
{application.id}{service.name ? `-${service.name}` : ''}</button
>
{/each}
</div>
{#if selectedService}
<div class="mx-auto max-w-4xl px-6 py-4 bg-coolgray-100 border border-coolgray-200 relative">
{#if usageLoading}
<button
id="streaming"
class="btn btn-sm bg-transparent border-none loading absolute top-0 left-0 text-xs"
/>
<Tooltip triggeredBy="#streaming">Streaming logs</Tooltip>
{/if}
<div class="text-center"> <div class="text-center">
<div class="stat w-64"> <div class="stat w-64">
<div class="stat-title">Used Memory / Memory Limit</div> <div class="stat-title">Used Memory / Memory Limit</div>
@ -55,4 +115,5 @@
<div class="stat-value text-xl">{usage?.NetIO}</div> <div class="stat-value text-xl">{usage?.NetIO}</div>
</div> </div>
</div> </div>
</div> </div>
{/if}

View File

@ -21,9 +21,11 @@
const { id } = $page.params; const { id } = $page.params;
let loading = false; let loading = {
let publicLoading = false; main: false,
public: false
};
let publicUrl = '';
let appendOnly = database.settings.appendOnly; let appendOnly = database.settings.appendOnly;
let databaseDefault: any; let databaseDefault: any;
@ -47,23 +49,46 @@
databaseDbUser = ''; databaseDbUser = '';
} }
} }
function generateUrl(): string { function generateUrl() {
return `${database.type}://${ const ipAddress = () => {
databaseDbUser ? databaseDbUser + ':' : '' if ($status.database.isPublic) {
}${databaseDbUserPassword}@${ if (database.destinationDocker.remoteEngine) {
$status.database.isPublic return database.destinationDocker.remoteIpAddress;
? database.destinationDocker.remoteEngine }
? database.destinationDocker.remoteIpAddress if ($appSession.ipv6) {
: $appSession.ipv4 return $appSession.ipv6;
: database.id }
}:${$status.database.isPublic ? database.publicPort : privatePort}/${databaseDefault}`; if ($appSession.ipv4) {
return $appSession.ipv4;
}
return '<Cannot determine public IP address>';
} else {
return database.id;
}
};
const user = () => {
if (databaseDbUser) {
return databaseDbUser + ':';
}
return '';
};
const port = () => {
if ($status.database.isPublic) {
return database.publicPort;
} else {
return privatePort;
}
};
publicUrl = `${
database.type
}://${user()}${databaseDbUserPassword}@${ipAddress()}:${port()}/${databaseDefault}`;
} }
async function changeSettings(name: any) { async function changeSettings(name: any) {
if (name !== 'appendOnly') { if (name !== 'appendOnly') {
if (publicLoading || !$status.database.isRunning) return; if (loading.public || !$status.database.isRunning) return;
} }
publicLoading = true; loading.public = true;
let data = { let data = {
isPublic: $status.database.isPublic, isPublic: $status.database.isPublic,
appendOnly appendOnly
@ -87,12 +112,12 @@
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {
publicLoading = false; loading.public = false;
} }
} }
async function handleSubmit() { async function handleSubmit() {
try { try {
loading = true; loading.main = true;
await post(`/databases/${id}`, { ...database, isRunning: $status.database.isRunning }); await post(`/databases/${id}`, { ...database, isRunning: $status.database.isRunning });
generateDbDetails(); generateDbDetails();
addToast({ addToast({
@ -102,7 +127,7 @@
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {
loading = false; loading.main = false;
} }
} }
</script> </script>
@ -115,9 +140,9 @@
<button <button
type="submit" type="submit"
class="btn btn-sm" class="btn btn-sm"
class:loading class:loading={loading.main}
class:bg-databases={!loading} class:bg-databases={!loading.main}
disabled={loading}>{$t('forms.save')}</button disabled={loading.main}>{$t('forms.save')}</button
> >
{/if} {/if}
</div> </div>
@ -175,7 +200,7 @@
readonly readonly
disabled disabled
name="publicPort" name="publicPort"
value={publicLoading value={loading.public
? 'Loading...' ? 'Loading...'
: $status.database.isPublic : $status.database.isPublic
? database.publicPort ? database.publicPort
@ -198,8 +223,8 @@
<EdgeDB {database} /> <EdgeDB {database} />
{/if} {/if}
<div class="flex flex-col space-y-2 mt-5"> <div class="flex flex-col space-y-2 mt-5">
<div> <div class="grid gap-2 grid-cols-2 auto-rows-max lg:px-10 px-2">
<label class="px-2" for="url" <label for="url"
>{$t('database.connection_string')} >{$t('database.connection_string')}
{#if !$status.database.isPublic && database.destinationDocker.remoteEngine} {#if !$status.database.isPublic && database.destinationDocker.remoteEngine}
<Explainer <Explainer
@ -207,18 +232,21 @@
/> />
{/if}</label {/if}</label
> >
<button class="btn btn-sm" on:click|preventDefault={generateUrl}
>Show Connection String</button
>
</div> </div>
<div class="lg:px-10 px-2"> <div class="lg:px-10 px-2">
{#if publicUrl}
<CopyPasswordField <CopyPasswordField
textarea={true} placeholder="Click on the button to generate URL"
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField={false}
id="url" id="url"
name="url" name="url"
readonly readonly
disabled disabled
value={publicLoading || loading ? 'Loading...' : generateUrl()} value={loading.public ? 'Loading...' : publicUrl}
/> />
{/if}
</div> </div>
</div> </div>
</form> </form>
@ -228,7 +256,7 @@
<div class="grid gap-2 grid-cols-2 auto-rows-max lg:px-10 px-2"> <div class="grid gap-2 grid-cols-2 auto-rows-max lg:px-10 px-2">
<Setting <Setting
id="isPublic" id="isPublic"
loading={publicLoading} loading={loading.public}
bind:setting={$status.database.isPublic} bind:setting={$status.database.isPublic}
on:click={() => changeSettings('isPublic')} on:click={() => changeSettings('isPublic')}
title={$t('database.set_public')} title={$t('database.set_public')}
@ -238,7 +266,7 @@
{#if database.type === 'redis'} {#if database.type === 'redis'}
<Setting <Setting
id="appendOnly" id="appendOnly"
loading={publicLoading} loading={loading.public}
bind:setting={appendOnly} bind:setting={appendOnly}
on:click={() => changeSettings('appendOnly')} on:click={() => changeSettings('appendOnly')}
title={$t('database.change_append_only_mode')} title={$t('database.change_append_only_mode')}

View File

@ -146,9 +146,29 @@
try { try {
numberOfGetStatus++; numberOfGetStatus++;
let isRunning = false; let isRunning = false;
let isDegraded = false;
if (buildPack) { if (buildPack) {
const response = await get(`/applications/${id}/status`); const response = await get(`/applications/${id}/status`);
isRunning = response.isRunning; if (response.length === 0) {
isRunning = false;
} else if (response.length === 1) {
isRunning = response[0].status.isRunning;
} else {
let overallStatus = false;
for (const oneStatus of response) {
if (oneStatus.status.isRunning) {
overallStatus = true;
} else {
isDegraded = true;
break;
}
}
if (overallStatus) {
isRunning = true;
} else {
isRunning = false;
}
}
} else if (typeof dualCerts !== 'undefined') { } else if (typeof dualCerts !== 'undefined') {
const response = await get(`/services/${id}/status`); const response = await get(`/services/${id}/status`);
isRunning = response.isRunning; isRunning = response.isRunning;
@ -156,9 +176,13 @@
const response = await get(`/databases/${id}/status`); const response = await get(`/databases/${id}/status`);
isRunning = response.isRunning; isRunning = response.isRunning;
} }
if (isRunning) { if (isRunning) {
status[id] = 'running'; status[id] = 'running';
return 'running'; return 'running';
} else if (isDegraded) {
status[id] = 'degraded';
return 'degraded';
} else { } else {
status[id] = 'stopped'; status[id] = 'stopped';
return 'stopped'; return 'stopped';
@ -213,6 +237,7 @@
(application.id && application.id.toLowerCase().includes($search.toLowerCase())) || (application.id && application.id.toLowerCase().includes($search.toLowerCase())) ||
(application.name && application.name.toLowerCase().includes($search.toLowerCase())) || (application.name && application.name.toLowerCase().includes($search.toLowerCase())) ||
(application.fqdn && application.fqdn.toLowerCase().includes($search.toLowerCase())) || (application.fqdn && application.fqdn.toLowerCase().includes($search.toLowerCase())) ||
(application.dockerComposeConfiguration && application.dockerComposeConfiguration.toLowerCase().includes($search.toLowerCase())) ||
(application.repository && (application.repository &&
application.repository.toLowerCase().includes($search.toLowerCase())) || application.repository.toLowerCase().includes($search.toLowerCase())) ||
(application.buildpack && (application.buildpack &&
@ -594,6 +619,11 @@
<span class="indicator-item badge bg-yellow-300 badge-sm" /> <span class="indicator-item badge bg-yellow-300 badge-sm" />
{:else if status[application.id] === 'running'} {:else if status[application.id] === 'running'}
<span class="indicator-item badge bg-success badge-sm" /> <span class="indicator-item badge bg-success badge-sm" />
{:else if status[application.id] === 'degraded'}
<span
class="indicator-item indicator-middle indicator-center badge bg-warning text-black font-bold badge-xl"
>Degraded</span
>
{:else} {:else}
<span class="indicator-item badge bg-error badge-sm" /> <span class="indicator-item badge bg-error badge-sm" />
{/if} {/if}
@ -613,7 +643,7 @@
<div class="h-10 text-xs"> <div class="h-10 text-xs">
{#if application?.fqdn} {#if application?.fqdn}
<h2>{application?.fqdn.replace('https://', '').replace('http://', '')}</h2> <h2>{application?.fqdn.replace('https://', '').replace('http://', '')}</h2>
{:else if !application.settings?.isBot && !application?.fqdn} {:else if (!application.settings?.isBot && !application?.fqdn) && application.buildPack !== 'compose'}
<h2 class="text-red-500">Not configured</h2> <h2 class="text-red-500">Not configured</h2>
{/if} {/if}
{#if application.destinationDocker?.name} {#if application.destinationDocker?.name}

View File

@ -27,10 +27,12 @@
<script lang="ts"> <script lang="ts">
export let types: any; export let types: any;
let search = '';
let filteredTypes = types;
import { page } from '$app/stores'; import { page } from '$app/stores';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import ServiceIcons from '$lib/components/svg/services/ServiceIcons.svelte'; import ServiceIcons from '$lib/components/svg/services/ServiceIcons.svelte';
@ -45,10 +47,50 @@
return errorNotification(error); return errorNotification(error);
} }
} }
function doSearch() {
filteredTypes = types.filter(
(type: any) =>
type.name.toLowerCase().includes(search.toLowerCase()) ||
type.labels.some((label: string) => label.toLowerCase().includes(search.toLowerCase()))
);
}
function cleanupSearch() {
search = '';
filteredTypes = types;
}
</script> </script>
<div class="flex flex-wrap justify-center"> <div class="container lg:mx-auto lg:p-0 px-8 pt-5">
{#each types as type} <div class="input-group flex w-full">
<div class="btn btn-square cursor-default no-animation hover:bg-error" on:click={cleanupSearch}>
<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" />
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</div>
<input
id="search"
class="input w-full"
type="text"
placeholder="Search for services"
bind:value={search}
on:input={() => doSearch()}
/>
</div>
</div>
<div class="container lg:mx-auto lg:pt-20 lg:p-0 px-8 pt-20">
<div class="flex flex-wrap justify-center gap-8">
{#each filteredTypes as type}
<div class="p-2"> <div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(type.name)}> <form on:submit|preventDefault={() => handleSubmit(type.name)}>
<button type="submit" class="box-selection relative text-xl font-bold hover:bg-pink-600"> <button type="submit" class="box-selection relative text-xl font-bold hover:bg-pink-600">
@ -59,3 +101,4 @@
</div> </div>
{/each} {/each}
</div> </div>
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

View File

@ -10,7 +10,7 @@ module.exports = {
"base-100": "#323232", "base-100": "#323232",
"base-200": "#242424", "base-200": "#242424",
"base-300": "#181818", "base-300": "#181818",
"primary": "#6d28d9", "primary": "#6B16ED",
"primary-content": "#fff", "primary-content": "#fff",
"secondary": "#343232", "secondary": "#343232",
"accent": "#343232", "accent": "#343232",

View File

@ -1,6 +1,38 @@
version: '3.8' version: '3.8'
services: services:
coolify:
build:
context: .
dockerfile: Dockerfile-dev
command: bash -c 'pnpm install && pnpm db:push && pnpm db:seed && pnpm dev'
environment:
- COOLIFY_CONTAINER_DEV=true
- COOLIFY_APP_ID=random-local-id
- COOLIFY_SECRET_KEY=12341234123412341234123412341234
- COOLIFY_DATABASE_URL=file:../db/dev.db
- GITPOD_WORKSPACE_URL=${GITPOD_WORKSPACE_URL}
- CODESANDBOX_HOST=${CODESANDBOX_HOST}
container_name: coolify
ports:
- target: 3000
published: 3000
protocol: tcp
mode: host
- target: 3001
published: 3001
protocol: tcp
mode: host
- target: 5555
published: 5555
protocol: tcp
mode: host
volumes:
- ./:/app
- '/var/run/docker.sock:/var/run/docker.sock'
- /tmp:/tmp
networks:
- coolify-infra
fluent-bit: fluent-bit:
image: coollabsio/coolify-fluent-bit:1.0.0 image: coollabsio/coolify-fluent-bit:1.0.0
command: /fluent-bit/bin/fluent-bit -c /fluent-bit/etc/fluent-bit-dev.conf command: /fluent-bit/bin/fluent-bit -c /fluent-bit/etc/fluent-bit-dev.conf
@ -8,7 +40,10 @@ services:
volumes: volumes:
- ./logs:/logs - ./logs:/logs
ports: ports:
- "24224:24224" - target: 24224
published: 24224
protocol: tcp
mode: host
networks: networks:
- coolify-infra - coolify-infra
networks: networks:

View File

@ -1,21 +1,24 @@
{ {
"name": "coolify", "name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.", "description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "3.10.14", "version": "3.10.15",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": "github:coollabsio/coolify", "repository": "github:coollabsio/coolify",
"scripts": { "scripts": {
"oc": "opencollective-setup", "oc": "opencollective-setup",
"translate": "pnpm run --filter i18n-converter translate", "translate": "pnpm run --filter i18n-converter translate",
"db:studio": "pnpm run --filter api db:studio", "db:studio": "pnpm run --filter api db:studio",
"db:studio:container": "docker exec coolify pnpm run --filter api db:studio",
"db:push": "pnpm run --filter api db:push", "db:push": "pnpm run --filter api db:push",
"db:seed": "pnpm run --filter api db:seed", "db:seed": "pnpm run --filter api db:seed",
"db:migrate": "pnpm run --filter api db:migrate", "db:migrate": "pnpm run --filter api db:migrate",
"db:migrate:container": "docker exec coolify pnpm run --filter api db:migrate",
"format": "run-p -l -n format:*", "format": "run-p -l -n format:*",
"format:api": "NODE_ENV=development pnpm run --filter api format", "format:api": "NODE_ENV=development pnpm run --filter api format",
"lint": "run-p -l -n lint:*", "lint": "run-p -l -n lint:*",
"lint:api": "NODE_ENV=development pnpm run --filter api lint", "lint:api": "NODE_ENV=development pnpm run --filter api lint",
"dev": "run-p -l -n dev:*", "dev:container": "docker-compose -f docker-compose-dev.yaml up --build || docker compose -f docker-compose-dev.yaml up --build",
"dev": "run-p -l -n dev:api dev:ui",
"dev:api": "NODE_ENV=development pnpm run --filter api dev", "dev:api": "NODE_ENV=development pnpm run --filter api dev",
"dev:ui": "NODE_ENV=development pnpm run --filter ui dev", "dev:ui": "NODE_ENV=development pnpm run --filter ui dev",
"build": "NODE_ENV=production run-p -n build:*", "build": "NODE_ENV=production run-p -n build:*",
@ -34,6 +37,7 @@
"docker", "docker",
"self-host", "self-host",
"iaas", "iaas",
"paas",
"heroku", "heroku",
"netlify", "netlify",
"open-source", "open-source",

View File

@ -104,6 +104,7 @@ importers:
node-os-utils: 1.3.7 node-os-utils: 1.3.7
p-all: 4.0.0 p-all: 4.0.0
p-throttle: 5.0.0 p-throttle: 5.0.0
prisma: 4.4.0
public-ip: 6.0.1 public-ip: 6.0.1
pump: 3.0.0 pump: 3.0.0
ssh-config: 4.1.6 ssh-config: 4.1.6
@ -120,7 +121,6 @@ importers:
eslint-plugin-prettier: 4.2.1_tgumt6uwl2md3n6uqnggd6wvce eslint-plugin-prettier: 4.2.1_tgumt6uwl2md3n6uqnggd6wvce
nodemon: 2.0.20 nodemon: 2.0.20
prettier: 2.7.1 prettier: 2.7.1
prisma: 4.4.0
rimraf: 3.0.2 rimraf: 3.0.2
tsconfig-paths: 4.1.0 tsconfig-paths: 4.1.0
typescript: 4.8.4 typescript: 4.8.4
@ -159,6 +159,7 @@ importers:
flowbite: 1.5.2 flowbite: 1.5.2
flowbite-svelte: 0.26.2 flowbite-svelte: 0.26.2
js-cookie: 3.0.1 js-cookie: 3.0.1
js-yaml: 4.1.0
p-limit: 4.0.0 p-limit: 4.0.0
postcss: 8.4.16 postcss: 8.4.16
prettier: 2.7.1 prettier: 2.7.1
@ -181,6 +182,7 @@ importers:
daisyui: 2.24.2_25hquoklqeoqwmt7fwvvcyxm5e daisyui: 2.24.2_25hquoklqeoqwmt7fwvvcyxm5e
dayjs: 1.11.5 dayjs: 1.11.5
js-cookie: 3.0.1 js-cookie: 3.0.1
js-yaml: 4.1.0
p-limit: 4.0.0 p-limit: 4.0.0
svelte-file-dropzone: 1.0.0 svelte-file-dropzone: 1.0.0
svelte-select: 4.4.7 svelte-select: 4.4.7
@ -532,6 +534,7 @@ packages:
/@prisma/engines/4.4.0: /@prisma/engines/4.4.0:
resolution: {integrity: sha512-Fpykccxlt9MHrAs/QpPGpI2nOiRxuLA+LiApgA59ibbf24YICZIMWd3SI2YD+q0IAIso0jCGiHhirAIbxK3RyQ==} resolution: {integrity: sha512-Fpykccxlt9MHrAs/QpPGpI2nOiRxuLA+LiApgA59ibbf24YICZIMWd3SI2YD+q0IAIso0jCGiHhirAIbxK3RyQ==}
requiresBuild: true requiresBuild: true
dev: false
/@rollup/pluginutils/4.2.1: /@rollup/pluginutils/4.2.1:
resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==}
@ -5078,6 +5081,7 @@ packages:
requiresBuild: true requiresBuild: true
dependencies: dependencies:
'@prisma/engines': 4.4.0 '@prisma/engines': 4.4.0
dev: false
/private/0.1.8: /private/0.1.8:
resolution: {integrity: sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==} resolution: {integrity: sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==}