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

@ -94,43 +94,46 @@ model TeamInvitation {
} }
model Application { model Application {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
fqdn String? fqdn String?
repository String? repository String?
configHash String? configHash String?
branch String? branch String?
buildPack String? buildPack String?
projectId Int? projectId Int?
port Int? port Int?
exposePort Int? exposePort Int?
installCommand String? installCommand String?
buildCommand String? buildCommand String?
startCommand String? startCommand String?
baseDirectory String? baseDirectory String?
publishDirectory String? publishDirectory String?
deploymentType String? deploymentType String?
phpModules String? phpModules String?
pythonWSGI String? pythonWSGI String?
pythonModule String? pythonModule String?
pythonVariable String? pythonVariable String?
dockerFileLocation String? dockerFileLocation String?
denoMainFile String? denoMainFile String?
denoOptions String? denoOptions String?
createdAt DateTime @default(now()) dockerComposeFile String?
updatedAt DateTime @updatedAt dockerComposeFileLocation String?
destinationDockerId String? dockerComposeConfiguration String?
gitSourceId String? createdAt DateTime @default(now())
baseImage String? updatedAt DateTime @updatedAt
baseBuildImage String? destinationDockerId String?
gitSource GitSource? @relation(fields: [gitSourceId], references: [id]) gitSourceId String?
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id]) baseImage String?
persistentStorage ApplicationPersistentStorage[] baseBuildImage String?
settings ApplicationSettings? gitSource GitSource? @relation(fields: [gitSourceId], references: [id])
secrets Secret[] destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
teams Team[] persistentStorage ApplicationPersistentStorage[]
connectedDatabase ApplicationConnectedDatabase? settings ApplicationSettings?
previewApplication PreviewApplication[] secrets Secret[]
teams Team[]
connectedDatabase ApplicationConnectedDatabase?
previewApplication PreviewApplication[]
} }
model PreviewApplication { model PreviewApplication {

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,112 +272,137 @@ 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 });
} }
try {
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker stop -t 0 ${imageId}` }) if (buildPack === 'compose') {
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker rm ${imageId}` }) try {
} catch (error) { await executeDockerCmd({
// dockerId: destinationDockerId,
} command: `docker ps -a --filter 'label=coolify.applicationId=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0`
const envs = [ })
`PORT=${port}` await executeDockerCmd({
]; dockerId: destinationDockerId,
if (secrets.length > 0) { command: `docker ps -a --filter 'label=coolify.applicationId=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker rm --force`
secrets.forEach((secret) => { })
if (pullmergeRequestId) { } catch (error) {
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) //
if (isSecretFound.length > 0) { }
envs.push(`${secret.name}=${isSecretFound[0].value}`); try {
} else { await executeDockerCmd({ debug, buildId, applicationId, dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` })
envs.push(`${secret.name}=${secret.value}`); await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
} await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId });
} else { await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } });
if (!secret.isPRMRSecret) { await prisma.application.update({
envs.push(`${secret.name}=${secret.value}`); 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);
} }
await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
const labels = makeLabelForStandaloneApplication({ } else {
applicationId, try {
fqdn, await executeDockerCmd({
name, dockerId: destinationDockerId,
type, command: `docker ps -a --filter 'label=com.docker.compose.service=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0`
pullmergeRequestId, })
buildPack, await executeDockerCmd({
repository, dockerId: destinationDockerId,
branch, command: `docker ps -a --filter 'label=com.docker.compose.service=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker rm --force`
projectId, })
port: exposePort ? `${exposePort}:${port}` : port, } catch (error) {
commit, //
installCommand, }
buildCommand, const envs = [
startCommand, `PORT=${port}`
baseDirectory, ];
publishDirectory if (secrets.length > 0) {
}); secrets.forEach((secret) => {
let envFound = false; if (pullmergeRequestId) {
try { const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
envFound = !!(await fs.stat(`${workdir}/.env`)); if (isSecretFound.length > 0) {
} catch (error) { envs.push(`${secret.name}=${isSecretFound[0].value}`);
// } else {
} envs.push(`${secret.name}=${secret.value}`);
try { }
await saveBuildLog({ line: 'Deployment started.', buildId, applicationId }); } else {
const composeVolumes = volumes.map((volume) => { if (!secret.isPRMRSecret) {
return { envs.push(`${secret.name}=${secret.value}`);
[`${volume.split(':')[0]}`]: { }
name: volume.split(':')[0]
}
};
});
const composeFile = {
version: '3.8',
services: {
[imageId]: {
image: `${applicationId}:${tag}`,
container_name: imageId,
volumes,
env_file: envFound ? [`${workdir}/.env`] : [],
labels,
depends_on: [],
expose: [port],
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
// logging: {
// driver: 'fluentd',
// },
...defaultComposeConfiguration(destinationDocker.network),
}
},
networks: {
[destinationDocker.network]: {
external: true
}
},
volumes: Object.assign({}, ...composeVolumes)
};
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` })
await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
} 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); await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
let envFound = false;
try {
envFound = !!(await fs.stat(`${workdir}/.env`));
} catch (error) {
//
}
try {
await saveBuildLog({ line: 'Deployment started.', buildId, applicationId });
const composeVolumes = volumes.map((volume) => {
return {
[`${volume.split(':')[0]}`]: {
name: volume.split(':')[0]
}
};
});
const composeFile = {
version: '3.8',
services: {
[imageId]: {
image: `${applicationId}:${tag}`,
container_name: imageId,
volumes,
env_file: envFound ? [`${workdir}/.env`] : [],
labels,
depends_on: [],
expose: [port],
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
...defaultComposeConfiguration(destinationDocker.network),
}
},
networks: {
[destinationDocker.network]: {
external: true
}
},
volumes: Object.assign({}, ...composeVolumes)
};
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` })
await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
} 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);
}
await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId });
await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } });
if (!pullmergeRequestId) await prisma.application.update({
where: { id: applicationId },
data: { configHash: currentHash }
});
} }
await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId });
await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } });
if (!pullmergeRequestId) await prisma.application.update({
where: { id: applicationId },
data: { configHash: currentHash }
});
} }
} }
catch (error) { catch (error) {

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({
@ -758,4 +762,4 @@ export async function buildCacheImageWithCargo(data, imageForBuild) {
Dockerfile.push('RUN cargo chef cook --release --recipe-path recipe.json'); Dockerfile.push('RUN cargo chef cook --release --recipe-path recipe.json');
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ ...data, isCache: true }); await buildImage({ ...data, isCache: true });
} }

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,54 +1773,77 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
], ],
...defaultComposeConfiguration(network), ...defaultComposeConfiguration(network),
}, },
[`${id}-usage-timeseries`]: {
image: `${image}:${version}`,
container_name: `${id}-usage`,
labels: makeLabelForServices('appwrite'),
entrypoint: "usage --type=timeseries",
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}-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",
container_name: `${id}-influxdb`,
volumes: [
`${id}-influxdb:/var/lib/influxdb:rw`
],
...defaultComposeConfiguration(network),
},
[`${id}-telegraf`]: {
image: "appwrite/telegraf:1.4.0",
container_name: `${id}-telegraf`,
environment: [
`_APP_INFLUXDB_HOST=${id}-influxdb`,
"_APP_INFLUXDB_PORT=8086",
`OPEN_RUNTIMES_NETWORK=${network}`,
],
...defaultComposeConfiguration(network),
}
}; };
dockerCompose[id].depends_on.push(`${id}-influxdb`);
dockerCompose[`${id}-usage`] = {
image: `${image}:${version}`,
container_name: `${id}-usage`,
labels: makeLabelForServices('appwrite'),
entrypoint: "usage",
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),
}
dockerCompose[`${id}-influxdb`] = {
image: "appwrite/influxdb:1.5.0",
container_name: `${id}-influxdb`,
volumes: [
`${id}-influxdb:/var/lib/influxdb:rw`
],
...defaultComposeConfiguration(network),
}
dockerCompose[`${id}-telegraf`] = {
image: "appwrite/telegraf:1.4.0",
container_name: `${id}-telegraf`,
environment: [
`_APP_INFLUXDB_HOST=${id}-influxdb`,
"_APP_INFLUXDB_PORT=8086",
`OPEN_RUNTIMES_NETWORK=${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) {
await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} pull` }) try {
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 isRunning = false; let payload = []
let isExited = false;
let isRestarting = false;
const application: any = await getApplicationFromDB(id, teamId); const application: any = await getApplicationFromDB(id, teamId);
if (application?.destinationDockerId) { if (application?.destinationDockerId) {
const status = await checkContainer({ dockerId: application.destinationDocker.id, container: id }); if (application.buildPack === 'compose') {
if (status?.found) { const { stdout: containers } = await executeDockerCmd({
isRunning = status.status.isRunning; dockerId: application.destinationDocker.id,
isExited = status.status.isExited; command:
isRestarting = status.status.isRestarting `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 isExited = false;
let isRestarting = false;
const status = await checkContainer({ dockerId: application.destinationDocker.id, container: id });
if (status?.found) {
isRunning = status.status.isRunning;
isExited = status.status.isExited;
isRestarting = status.status.isRestarting
payload.push({
name: id,
status: {
isRunning,
isExited,
isRestarting
}
})
}
} }
} }
return { return payload
isRunning,
isRestarting,
isExited,
};
} 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',
@ -349,14 +358,14 @@ export const buildPacks = [
color: 'bg-green-700', color: 'bg-green-700',
isCoolifyBuildPack: true, isCoolifyBuildPack: true,
}, },
{ {
name: 'heroku', name: 'heroku',
type: 'base', type: 'base',
fancyName: 'Heroku', fancyName: 'Heroku',
hoverColor: 'hover:bg-purple-700', hoverColor: 'hover:bg-purple-700',
color: 'bg-purple-700', color: 'bg-purple-700',
isHerokuBuildPack: true, isHerokuBuildPack: true,
} }
]; ];
export const scanningTemplates = { export const scanningTemplates = {
'@sveltejs/kit': { '@sveltejs/kit': {

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,38 +338,34 @@
<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 on:click={restartApplication}
id="restart" type="submit"
on:click={restartApplication} disabled={!$isDeploymentEnabled}
type="submit" class="btn btn-sm gap-2"
disabled={!$isDeploymentEnabled}
class="icons bg-transparent"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
> >
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <svg
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" /> xmlns="http://www.w3.org/2000/svg"
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" /> class="w-6 h-6"
</svg> viewBox="0 0 24 24"
</button> stroke-width="1.5"
<Tooltip triggeredBy="#restart">Restart (useful to change secrets)</Tooltip> stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" />
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" />
</svg> Restart
</button>
{/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,33 +384,80 @@
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
on:click={stopApplication}
type="submit"
disabled={!$isDeploymentEnabled}
class="btn btn-sm btn-error gap-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 "
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<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 <button
class="icons flex items-center font-bold" class="btn btn-sm gap-2"
class:btn-primary={$status.application.overallStatus !== 'degraded'}
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled}
on:click={() => handleDeploySubmit(false)} on:click={() => handleDeploySubmit(false)}
> >
<svg {#if $status.application.overallStatus !== 'degraded'}
xmlns="http://www.w3.org/2000/svg" <svg
class="w-6 h-6 mr-2 text-green-500" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" class="w-6 h-6"
stroke-width="1.5" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="1.5"
fill="none" stroke="currentColor"
stroke-linecap="round" fill="none"
stroke-linejoin="round" stroke-linecap="round"
> stroke-linejoin="round"
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> >
<path d="M7 4v16l13 -8z" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
</svg> <path d="M7 4v16l13 -8z" />
Deploy </svg>
{: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( scanRepository(isPublicRepository);
`${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}`, // let htmlUrl = application.gitSource.htmlUrl;
'GitLab', // const left = screen.width / 2 - 1020 / 2;
'resizable=1, scrollbars=1, fullscreen=0, height=618, width=1020,top=' + // const top = screen.height / 2 - 618 / 2;
top + // const newWindow = open(
', left=' + // `${htmlUrl}/oauth/authorize?client_id=${
left + // application.gitSource.gitlabApp.appId
', toolbar=0, menubar=0, status=0' // }&redirect_uri=${getAPIUrl()}/webhooks/gitlab&response_type=code&scope=api+email+read_repository&state=${
); // $page.params.id
const timer = setInterval(() => { // }`,
if (newWindow?.closed) { // 'GitLab',
clearInterval(timer); // 'resizable=1, scrollbars=1, fullscreen=0, height=618, width=1020,top=' +
window.location.reload(); // top +
} // ', left=' +
}, 100); // 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>

File diff suppressed because it is too large Load Diff

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,50 +115,69 @@
<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">
{#if logs.length === 0} {#each services as service}
<div class="text-xl font-bold tracking-tighter">{$t('application.build.waiting_logs')}</div> <button
{:else} on:click={() => selectService(service, true)}
<div class="relative w-full"> class:bg-primary={selectedService ===
<div class="flex justify-start sticky space-x-2 pb-2"> `${application.id}${service.name ? `-${service.name}` : ''}`}
<button class:bg-coolgray-200={selectedService !==
on:click={followBuild} `${application.id}${service.name ? `-${service.name}` : ''}`}
class="btn btn-sm bg-coollabs" class="w-full rounded p-5 hover:bg-primary font-bold"
class:bg-coolgray-300={followingLogs} >
class:text-applications={followingLogs} {application.id}{service.name ? `-${service.name}` : ''}</button
> >
<svg {/each}
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 mr-2"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="12" cy="12" r="9" />
<line x1="8" y1="12" x2="12" y2="16" />
<line x1="12" y1="8" x2="12" y2="16" />
<line x1="16" y1="12" x2="12" y2="16" />
</svg>
{followingLogs ? 'Following Logs...' : 'Follow Logs'}
</button>
{#if loadLogsInterval}
<button id="streaming" class="btn btn-sm bg-transparent border-none loading" />
<Tooltip triggeredBy="#streaming">Streaming logs</Tooltip>
{/if}
</div>
<div
bind:this={logsEl}
on:scroll={detect}
class="font-mono w-full bg-coolgray-100 border border-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded mb-20 flex flex-col scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1"
>
{#each logs as log}
<p>{log + '\n'}</p>
{/each}
</div>
</div>
{/if}
</div> </div>
{#if selectedService}
<div class="flex flex-row justify-center space-x-2">
{#if logs.length === 0}
{#if noContainer}
<div class="text-xl font-bold tracking-tighter">Container not found / exited.</div>
{/if}
{:else}
<div class="relative w-full">
<div class="flex justify-start sticky space-x-2 pb-2">
<button
on:click={followBuild}
class="btn btn-sm bg-coollabs"
class:bg-coolgray-300={followingLogs}
class:text-applications={followingLogs}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 mr-2"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="12" cy="12" r="9" />
<line x1="8" y1="12" x2="12" y2="16" />
<line x1="12" y1="8" x2="12" y2="16" />
<line x1="16" y1="12" x2="12" y2="16" />
</svg>
{followingLogs ? 'Following Logs...' : 'Follow Logs'}
</button>
{#if loadLogsInterval}
<button id="streaming" class="btn btn-sm bg-transparent border-none loading" />
<Tooltip triggeredBy="#streaming">Streaming logs</Tooltip>
{/if}
</div>
<div
bind:this={logsEl}
on:scroll={detect}
class="font-mono w-full bg-coolgray-100 border border-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded mb-20 flex flex-col scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1"
>
{#each logs as log}
<p>{log + '\n'}</p>
{/each}
</div>
</div>
{/if}
</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,21 +76,44 @@
<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">
<div class="text-center"> {#each services as service}
<div class="stat w-64"> <button
<div class="stat-title">Used Memory / Memory Limit</div> on:click={() => selectService(service, true)}
<div class="stat-value text-xl">{usage?.MemUsage}</div> class:bg-primary={selectedService ===
</div> `${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="stat w-64">
<div class="stat-title">Used Memory / Memory Limit</div>
<div class="stat-value text-xl">{usage?.MemUsage}</div>
</div>
<div class="stat w-64"> <div class="stat w-64">
<div class="stat-title">Used CPU</div> <div class="stat-title">Used CPU</div>
<div class="stat-value text-xl">{usage?.CPUPerc}</div> <div class="stat-value text-xl">{usage?.CPUPerc}</div>
</div> </div>
<div class="stat w-64"> <div class="stat w-64">
<div class="stat-title">Network IO</div> <div class="stat-title">Network IO</div>
<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">
<CopyPasswordField {#if publicUrl}
textarea={true} <CopyPasswordField
placeholder={$t('forms.generated_automatically_after_start')} placeholder="Click on the button to generate URL"
isPasswordField={false} id="url"
id="url" name="url"
name="url" readonly
readonly disabled
disabled value={loading.public ? 'Loading...' : publicUrl}
value={publicLoading || loading ? 'Loading...' : generateUrl()} />
/> {/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==}