commit
fb955e15f4
34
.github/workflows/production-release.yml
vendored
34
.github/workflows/production-release.yml
vendored
@ -5,7 +5,7 @@ on:
|
||||
types: [released]
|
||||
|
||||
jobs:
|
||||
arm64-build:
|
||||
arm64:
|
||||
runs-on: [self-hosted, arm64]
|
||||
steps:
|
||||
- name: Checkout
|
||||
@ -31,7 +31,7 @@ jobs:
|
||||
tags: coollabsio/coolify:${{steps.package-version.outputs.current-version}}-arm64
|
||||
cache-from: type=registry,ref=coollabsio/coolify:buildcache-arm64
|
||||
cache-to: type=registry,ref=coollabsio/coolify:buildcache-arm64,mode=max
|
||||
amd64-build:
|
||||
amd64:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@ -57,9 +57,35 @@ jobs:
|
||||
tags: coollabsio/coolify:${{steps.package-version.outputs.current-version}}-amd64
|
||||
cache-from: type=registry,ref=coollabsio/coolify:buildcache-amd64
|
||||
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:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [amd64-build, arm64-build]
|
||||
needs: [amd64, arm64, aarch64]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
@ -77,7 +103,7 @@ jobs:
|
||||
id: package-version
|
||||
- name: Create & publish manifest
|
||||
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}}
|
||||
- uses: sarisia/actions-status-discord@v1
|
||||
if: always()
|
||||
|
10
.github/workflows/staging-release.yml
vendored
10
.github/workflows/staging-release.yml
vendored
@ -6,7 +6,7 @@ on:
|
||||
- next
|
||||
|
||||
jobs:
|
||||
arm64-making-something-cool:
|
||||
arm64:
|
||||
runs-on: [self-hosted, arm64]
|
||||
steps:
|
||||
- name: Checkout
|
||||
@ -34,7 +34,7 @@ jobs:
|
||||
tags: coollabsio/coolify:next-arm64
|
||||
cache-from: type=registry,ref=coollabsio/coolify:buildcache-next-arm64
|
||||
cache-to: type=registry,ref=coollabsio/coolify:buildcache-next-arm64,mode=max
|
||||
amd64-making-something-cool:
|
||||
amd64:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@ -59,12 +59,12 @@ jobs:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
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-to: type=registry,ref=coollabsio/coolify:buildcache-next-amd64,mode=max
|
||||
merge-manifest-to-be-cool:
|
||||
merge-manifest:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [arm64-making-something-cool, amd64-making-something-cool]
|
||||
needs: [arm64, amd64]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
@ -5,19 +5,27 @@ # Contribution
|
||||
You can ask for guidance anytime on our Discord server in the #contribution channel.
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
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
|
||||
> 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 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.
|
||||
|
21
Dockerfile
21
Dockerfile
@ -1,5 +1,4 @@
|
||||
ARG PNPM_VERSION=7.11.0
|
||||
ARG NPM_VERSION=8.19.1
|
||||
|
||||
FROM node:18-slim as build
|
||||
WORKDIR /app
|
||||
@ -17,20 +16,26 @@ WORKDIR /app
|
||||
ENV NODE_ENV production
|
||||
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-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/
|
||||
# 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/others/fluentbit/ ./fluentbit
|
||||
|
31
Dockerfile-dev
Normal file
31
Dockerfile-dev
Normal 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
|
@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Application" ADD COLUMN "dockerComposeFile" TEXT;
|
||||
ALTER TABLE "Application" ADD COLUMN "dockerComposeFileLocation" TEXT;
|
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Application" ADD COLUMN "dockerComposeConfiguration" TEXT;
|
@ -117,6 +117,9 @@ model Application {
|
||||
dockerFileLocation String?
|
||||
denoMainFile String?
|
||||
denoOptions String?
|
||||
dockerComposeFile String?
|
||||
dockerComposeFileLocation String?
|
||||
dockerComposeConfiguration String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
destinationDockerId String?
|
||||
|
@ -6,11 +6,14 @@ import cookie from '@fastify/cookie';
|
||||
import multipart from '@fastify/multipart';
|
||||
import path, { join } from 'path';
|
||||
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 { compareVersions } from 'compare-versions';
|
||||
import Graceful from '@ladjs/graceful'
|
||||
import axios from 'axios';
|
||||
import fs from 'fs/promises';
|
||||
import { verifyRemoteDockerEngineFn } from './routes/api/v1/destinations/handlers';
|
||||
import { checkContainer } from './lib/docker';
|
||||
declare module 'fastify' {
|
||||
interface FastifyInstance {
|
||||
config: {
|
||||
@ -72,7 +75,6 @@ const host = '0.0.0.0';
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
const options = {
|
||||
schema,
|
||||
dotenv: true
|
||||
@ -131,29 +133,26 @@ const host = '0.0.0.0';
|
||||
if (!scheduler.workers.has('deployApplication')) {
|
||||
scheduler.run('deployApplication');
|
||||
}
|
||||
if (!scheduler.workers.has('infrastructure')) {
|
||||
scheduler.run('infrastructure');
|
||||
}
|
||||
}, 2000)
|
||||
|
||||
// autoUpdater
|
||||
setInterval(async () => {
|
||||
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:autoUpdater")
|
||||
await autoUpdater()
|
||||
}, 60000 * 15)
|
||||
|
||||
// cleanupStorage
|
||||
setInterval(async () => {
|
||||
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupStorage")
|
||||
await cleanupStorage()
|
||||
}, 60000 * 10)
|
||||
|
||||
// checkProxies and checkFluentBit
|
||||
setInterval(async () => {
|
||||
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:checkProxies")
|
||||
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:checkFluentBit")
|
||||
await checkProxies();
|
||||
await checkFluentBit();
|
||||
}, 10000)
|
||||
|
||||
setInterval(async () => {
|
||||
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:copySSLCertificates")
|
||||
await copySSLCertificates();
|
||||
}, 2000)
|
||||
|
||||
await Promise.all([
|
||||
@ -165,9 +164,6 @@ const host = '0.0.0.0';
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
})();
|
||||
|
||||
|
||||
@ -227,3 +223,237 @@ async function configureRemoteDockers() {
|
||||
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)
|
||||
}
|
||||
}
|
@ -85,6 +85,7 @@ import * as buildpacks from '../lib/buildPacks';
|
||||
baseDirectory,
|
||||
publishDirectory,
|
||||
dockerFileLocation,
|
||||
dockerComposeConfiguration,
|
||||
denoMainFile
|
||||
} = application
|
||||
const currentHash = crypto
|
||||
@ -112,17 +113,6 @@ import * as buildpacks from '../lib/buildPacks';
|
||||
)
|
||||
.digest('hex');
|
||||
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 domain = getDomain(fqdn);
|
||||
const volumes =
|
||||
@ -138,6 +128,10 @@ import * as buildpacks from '../lib/buildPacks';
|
||||
repository = sourceRepository || repository;
|
||||
}
|
||||
|
||||
try {
|
||||
dockerComposeConfiguration = JSON.parse(dockerComposeConfiguration)
|
||||
} catch (error) { }
|
||||
|
||||
let deployNeeded = true;
|
||||
let destinationType;
|
||||
|
||||
@ -212,17 +206,37 @@ import * as buildpacks from '../lib/buildPacks';
|
||||
//
|
||||
}
|
||||
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 (!imageFound || deployNeeded) {
|
||||
// if (true) {
|
||||
if (buildpacks[buildPack])
|
||||
await buildpacks[buildPack]({
|
||||
dockerId: destinationDocker.id,
|
||||
network: destinationDocker.network,
|
||||
buildId,
|
||||
applicationId,
|
||||
domain,
|
||||
name,
|
||||
type,
|
||||
volumes,
|
||||
labels,
|
||||
pullmergeRequestId,
|
||||
buildPack,
|
||||
repository,
|
||||
@ -244,11 +258,12 @@ import * as buildpacks from '../lib/buildPacks';
|
||||
pythonModule,
|
||||
pythonVariable,
|
||||
dockerFileLocation,
|
||||
dockerComposeConfiguration,
|
||||
denoMainFile,
|
||||
denoOptions,
|
||||
baseImage,
|
||||
baseBuildImage,
|
||||
deploymentType
|
||||
deploymentType,
|
||||
});
|
||||
else {
|
||||
await saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId });
|
||||
@ -257,9 +272,53 @@ import * as buildpacks from '../lib/buildPacks';
|
||||
} else {
|
||||
await saveBuildLog({ line: 'Build image already available - no rebuild required.', buildId, applicationId });
|
||||
}
|
||||
|
||||
if (buildPack === 'compose') {
|
||||
try {
|
||||
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker stop -t 0 ${imageId}` })
|
||||
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker rm ${imageId}` })
|
||||
await executeDockerCmd({
|
||||
dockerId: destinationDockerId,
|
||||
command: `docker ps -a --filter 'label=coolify.applicationId=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0`
|
||||
})
|
||||
await executeDockerCmd({
|
||||
dockerId: destinationDockerId,
|
||||
command: `docker ps -a --filter 'label=coolify.applicationId=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker rm --force`
|
||||
})
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
try {
|
||||
await executeDockerCmd({ debug, buildId, applicationId, dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` })
|
||||
await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
|
||||
await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId });
|
||||
await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } });
|
||||
await prisma.application.update({
|
||||
where: { id: applicationId },
|
||||
data: { configHash: currentHash }
|
||||
});
|
||||
} catch (error) {
|
||||
await saveBuildLog({ line: error, buildId, applicationId });
|
||||
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } })
|
||||
if (foundBuild) {
|
||||
await prisma.build.update({
|
||||
where: { id: buildId },
|
||||
data: {
|
||||
status: 'failed'
|
||||
}
|
||||
});
|
||||
}
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
} else {
|
||||
try {
|
||||
await executeDockerCmd({
|
||||
dockerId: destinationDockerId,
|
||||
command: `docker ps -a --filter 'label=com.docker.compose.service=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0`
|
||||
})
|
||||
await executeDockerCmd({
|
||||
dockerId: destinationDockerId,
|
||||
command: `docker ps -a --filter 'label=com.docker.compose.service=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker rm --force`
|
||||
})
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
@ -283,24 +342,7 @@ import * as buildpacks from '../lib/buildPacks';
|
||||
});
|
||||
}
|
||||
await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
|
||||
const labels = makeLabelForStandaloneApplication({
|
||||
applicationId,
|
||||
fqdn,
|
||||
name,
|
||||
type,
|
||||
pullmergeRequestId,
|
||||
buildPack,
|
||||
repository,
|
||||
branch,
|
||||
projectId,
|
||||
port: exposePort ? `${exposePort}:${port}` : port,
|
||||
commit,
|
||||
installCommand,
|
||||
buildCommand,
|
||||
startCommand,
|
||||
baseDirectory,
|
||||
publishDirectory
|
||||
});
|
||||
|
||||
let envFound = false;
|
||||
try {
|
||||
envFound = !!(await fs.stat(`${workdir}/.env`));
|
||||
@ -328,9 +370,6 @@ import * as buildpacks from '../lib/buildPacks';
|
||||
depends_on: [],
|
||||
expose: [port],
|
||||
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
|
||||
// logging: {
|
||||
// driver: 'fluentd',
|
||||
// },
|
||||
...defaultComposeConfiguration(destinationDocker.network),
|
||||
}
|
||||
},
|
||||
@ -365,6 +404,7 @@ import * as buildpacks from '../lib/buildPacks';
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } })
|
||||
if (foundBuild) {
|
||||
|
@ -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);
|
||||
})();
|
@ -468,9 +468,9 @@ export const saveBuildLog = async ({
|
||||
line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
|
||||
}
|
||||
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}`);
|
||||
}
|
||||
try {
|
||||
@ -580,7 +580,8 @@ export async function buildImage({
|
||||
dockerId,
|
||||
isCache = false,
|
||||
debug = false,
|
||||
dockerFileLocation = '/Dockerfile'
|
||||
dockerFileLocation = '/Dockerfile',
|
||||
commit
|
||||
}) {
|
||||
if (isCache) {
|
||||
await saveBuildLog({ line: `Building cache image started.`, buildId, applicationId });
|
||||
@ -596,7 +597,9 @@ export async function buildImage({
|
||||
}
|
||||
const dockerFile = isCache ? `${dockerFileLocation}-cache` : `${dockerFileLocation}`
|
||||
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 } })
|
||||
if (status === 'canceled') {
|
||||
throw new Error('Deployment canceled.')
|
||||
@ -634,6 +637,7 @@ export function makeLabelForStandaloneApplication({
|
||||
return [
|
||||
'coolify.managed=true',
|
||||
`coolify.version=${version}`,
|
||||
`coolify.applicationId=${applicationId}`,
|
||||
`coolify.type=standalone-application`,
|
||||
`coolify.configuration=${base64Encode(
|
||||
JSON.stringify({
|
||||
|
100
apps/api/src/lib/buildPacks/compose.ts
Normal file
100
apps/api/src/lib/buildPacks/compose.ts
Normal 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 });
|
||||
}
|
@ -49,7 +49,7 @@ const createDockerfile = async (data, image): Promise<void> => {
|
||||
Dockerfile.push(`RUN deno cache ${denoMainFile}`);
|
||||
Dockerfile.push(`ENV NO_COLOR true`);
|
||||
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'));
|
||||
};
|
||||
|
||||
|
@ -16,6 +16,7 @@ import python from './python';
|
||||
import deno from './deno';
|
||||
import laravel from './laravel';
|
||||
import heroku from './heroku';
|
||||
import compose from './compose'
|
||||
|
||||
export {
|
||||
node,
|
||||
@ -35,5 +36,6 @@ export {
|
||||
python,
|
||||
deno,
|
||||
laravel,
|
||||
heroku
|
||||
heroku,
|
||||
compose
|
||||
};
|
||||
|
@ -20,7 +20,7 @@ import { scheduler } from './scheduler';
|
||||
import { supportedServiceTypesAndVersions } from './services/supportedVersions';
|
||||
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';
|
||||
|
||||
const algorithm = 'aes-256-ctr';
|
||||
@ -264,7 +264,9 @@ export async function isDomainConfigured({
|
||||
where: {
|
||||
OR: [
|
||||
{ fqdn: { endsWith: `//${nakedDomain}` } },
|
||||
{ fqdn: { endsWith: `//www.${nakedDomain}` } }
|
||||
{ fqdn: { endsWith: `//www.${nakedDomain}` } },
|
||||
{ dockerComposeConfiguration: { contains: `//${nakedDomain}` } },
|
||||
{ dockerComposeConfiguration: { contains: `//www.${nakedDomain}` } }
|
||||
],
|
||||
id: { not: id },
|
||||
destinationDocker: {
|
||||
@ -598,7 +600,7 @@ export async function executeDockerCmd({ debug, buildId, applicationId, dockerId
|
||||
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 execaCommand(command, { env: { DOCKER_BUILDKIT: "1", DOCKER_HOST: engine }, shell: true })
|
||||
|
@ -87,6 +87,9 @@ export async function removeContainer({
|
||||
await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` })
|
||||
await executeDockerCmd({ dockerId, command: `docker rm ${id}` })
|
||||
}
|
||||
if (JSON.parse(stdout).Status === 'exited') {
|
||||
await executeDockerCmd({ dockerId, command: `docker rm ${id}` })
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
@ -73,6 +73,4 @@ export default async function ({
|
||||
const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`);
|
||||
|
||||
return commit.replace('\n', '');
|
||||
|
||||
|
||||
}
|
||||
|
@ -11,15 +11,14 @@ const options: any = {
|
||||
defaultExtension: 'js',
|
||||
logger: new Cabin(),
|
||||
// logger: false,
|
||||
workerMessageHandler: async ({ name, message }) => {
|
||||
if (name === 'deployApplication' && message?.deploying) {
|
||||
if (scheduler.workers.has('autoUpdater') || scheduler.workers.has('cleanupStorage')) {
|
||||
scheduler.workers.get('deployApplication').postMessage('cancel')
|
||||
}
|
||||
}
|
||||
},
|
||||
// workerMessageHandler: async ({ name, message }) => {
|
||||
// if (name === 'deployApplication' && message?.deploying) {
|
||||
// if (scheduler.workers.has('autoUpdater') || scheduler.workers.has('cleanupStorage')) {
|
||||
// scheduler.workers.get('deployApplication').postMessage('cancel')
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
jobs: [
|
||||
{ name: 'infrastructure' },
|
||||
{ name: 'deployApplication' },
|
||||
],
|
||||
};
|
||||
|
@ -1410,6 +1410,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
|
||||
depends_on: [
|
||||
`${id}-mariadb`,
|
||||
`${id}-redis`,
|
||||
`${id}-influxdb`,
|
||||
],
|
||||
environment: [
|
||||
"_APP_ENV=production",
|
||||
@ -1772,14 +1773,11 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
|
||||
],
|
||||
...defaultComposeConfiguration(network),
|
||||
},
|
||||
|
||||
};
|
||||
dockerCompose[id].depends_on.push(`${id}-influxdb`);
|
||||
dockerCompose[`${id}-usage`] = {
|
||||
[`${id}-usage-timeseries`]: {
|
||||
image: `${image}:${version}`,
|
||||
container_name: `${id}-usage`,
|
||||
labels: makeLabelForServices('appwrite'),
|
||||
entrypoint: "usage",
|
||||
entrypoint: "usage --type=timeseries",
|
||||
depends_on: [
|
||||
`${id}-mariadb`,
|
||||
`${id}-influxdb`,
|
||||
@ -1800,16 +1798,42 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
|
||||
...secrets
|
||||
],
|
||||
...defaultComposeConfiguration(network),
|
||||
}
|
||||
dockerCompose[`${id}-influxdb`] = {
|
||||
},
|
||||
[`${id}-usage-database`]: {
|
||||
image: `${image}:${version}`,
|
||||
container_name: `${id}-usage-database`,
|
||||
labels: makeLabelForServices('appwrite'),
|
||||
entrypoint: "usage --type=database",
|
||||
depends_on: [
|
||||
`${id}-mariadb`,
|
||||
`${id}-influxdb`,
|
||||
],
|
||||
environment: [
|
||||
"_APP_ENV=production",
|
||||
`_APP_OPENSSL_KEY_V1=${opensslKeyV1}`,
|
||||
`_APP_DB_HOST=${mariadbHost}`,
|
||||
`_APP_DB_PORT=${mariadbPort}`,
|
||||
`_APP_DB_SCHEMA=${mariadbDatabase}`,
|
||||
`_APP_DB_USER=${mariadbUser}`,
|
||||
`_APP_DB_PASS=${mariadbPassword}`,
|
||||
`_APP_INFLUXDB_HOST=${id}-influxdb`,
|
||||
"_APP_INFLUXDB_PORT=8086",
|
||||
`_APP_REDIS_HOST=${id}-redis`,
|
||||
"_APP_REDIS_PORT=6379",
|
||||
`OPEN_RUNTIMES_NETWORK=${network}`,
|
||||
...secrets
|
||||
],
|
||||
...defaultComposeConfiguration(network),
|
||||
},
|
||||
[`${id}-influxdb`]: {
|
||||
image: "appwrite/influxdb:1.5.0",
|
||||
container_name: `${id}-influxdb`,
|
||||
volumes: [
|
||||
`${id}-influxdb:/var/lib/influxdb:rw`
|
||||
],
|
||||
...defaultComposeConfiguration(network),
|
||||
}
|
||||
dockerCompose[`${id}-telegraf`] = {
|
||||
},
|
||||
[`${id}-telegraf`]: {
|
||||
image: "appwrite/telegraf:1.4.0",
|
||||
container_name: `${id}-telegraf`,
|
||||
environment: [
|
||||
@ -1819,7 +1843,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
|
||||
],
|
||||
...defaultComposeConfiguration(network),
|
||||
}
|
||||
|
||||
};
|
||||
const composeFile: any = {
|
||||
version: '3.8',
|
||||
services: dockerCompose,
|
||||
@ -1868,7 +1892,9 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
|
||||
}
|
||||
}
|
||||
async function startServiceContainers(dockerId, composeFileDestination) {
|
||||
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} create` })
|
||||
await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} start` })
|
||||
|
@ -29,7 +29,8 @@ export const supportedServiceTypesAndVersions = [
|
||||
recommendedVersion: 'stable',
|
||||
ports: {
|
||||
main: 8000
|
||||
}
|
||||
},
|
||||
labels: ['analytics', 'plausible', 'plausible-analytics', 'gdpr', 'no-cookie']
|
||||
},
|
||||
{
|
||||
name: 'nocodb',
|
||||
@ -39,7 +40,8 @@ export const supportedServiceTypesAndVersions = [
|
||||
recommendedVersion: 'latest',
|
||||
ports: {
|
||||
main: 8080
|
||||
}
|
||||
},
|
||||
labels: ['nocodb', 'airtable', 'database']
|
||||
},
|
||||
{
|
||||
name: 'minio',
|
||||
@ -49,7 +51,8 @@ export const supportedServiceTypesAndVersions = [
|
||||
recommendedVersion: 'latest',
|
||||
ports: {
|
||||
main: 9001
|
||||
}
|
||||
},
|
||||
labels: ['minio', 's3', 'storage']
|
||||
},
|
||||
{
|
||||
name: 'vscodeserver',
|
||||
@ -59,7 +62,8 @@ export const supportedServiceTypesAndVersions = [
|
||||
recommendedVersion: 'latest',
|
||||
ports: {
|
||||
main: 8080
|
||||
}
|
||||
},
|
||||
labels: ['vscodeserver', 'vscode', 'code-server', 'ide']
|
||||
},
|
||||
{
|
||||
name: 'wordpress',
|
||||
@ -70,7 +74,8 @@ export const supportedServiceTypesAndVersions = [
|
||||
recommendedVersion: 'latest',
|
||||
ports: {
|
||||
main: 80
|
||||
}
|
||||
},
|
||||
labels: ['wordpress', 'blog', 'cms']
|
||||
},
|
||||
{
|
||||
name: 'vaultwarden',
|
||||
@ -80,7 +85,8 @@ export const supportedServiceTypesAndVersions = [
|
||||
recommendedVersion: 'latest',
|
||||
ports: {
|
||||
main: 80
|
||||
}
|
||||
},
|
||||
labels: ['vaultwarden', 'password-manager', 'passwords']
|
||||
},
|
||||
{
|
||||
name: 'languagetool',
|
||||
@ -90,7 +96,8 @@ export const supportedServiceTypesAndVersions = [
|
||||
recommendedVersion: 'latest',
|
||||
ports: {
|
||||
main: 8010
|
||||
}
|
||||
},
|
||||
labels: ['languagetool', 'grammar', 'spell-checker']
|
||||
},
|
||||
{
|
||||
name: 'n8n',
|
||||
@ -100,7 +107,8 @@ export const supportedServiceTypesAndVersions = [
|
||||
recommendedVersion: 'latest',
|
||||
ports: {
|
||||
main: 5678
|
||||
}
|
||||
},
|
||||
labels: ['n8n', 'workflow', 'automation', 'ifttt', 'zapier', 'nodered']
|
||||
},
|
||||
{
|
||||
name: 'uptimekuma',
|
||||
@ -110,7 +118,8 @@ export const supportedServiceTypesAndVersions = [
|
||||
recommendedVersion: 'latest',
|
||||
ports: {
|
||||
main: 3001
|
||||
}
|
||||
},
|
||||
labels: ['uptimekuma', 'uptime', 'monitoring']
|
||||
},
|
||||
{
|
||||
name: 'ghost',
|
||||
@ -121,7 +130,8 @@ export const supportedServiceTypesAndVersions = [
|
||||
recommendedVersion: 'latest',
|
||||
ports: {
|
||||
main: 2368
|
||||
}
|
||||
},
|
||||
labels: ['ghost', 'blog', 'cms']
|
||||
},
|
||||
{
|
||||
name: 'meilisearch',
|
||||
@ -132,7 +142,8 @@ export const supportedServiceTypesAndVersions = [
|
||||
recommendedVersion: 'latest',
|
||||
ports: {
|
||||
main: 7700
|
||||
}
|
||||
},
|
||||
labels: ['meilisearch', 'search', 'search-engine']
|
||||
},
|
||||
{
|
||||
name: 'umami',
|
||||
@ -143,7 +154,8 @@ export const supportedServiceTypesAndVersions = [
|
||||
recommendedVersion: 'postgresql-latest',
|
||||
ports: {
|
||||
main: 3000
|
||||
}
|
||||
},
|
||||
labels: ['umami', 'analytics', 'gdpr', 'no-cookie']
|
||||
},
|
||||
{
|
||||
name: 'hasura',
|
||||
@ -154,7 +166,8 @@ export const supportedServiceTypesAndVersions = [
|
||||
recommendedVersion: 'v2.10.0',
|
||||
ports: {
|
||||
main: 8080
|
||||
}
|
||||
},
|
||||
labels: ['hasura', 'graphql', 'database']
|
||||
},
|
||||
{
|
||||
name: 'fider',
|
||||
@ -165,7 +178,8 @@ export const supportedServiceTypesAndVersions = [
|
||||
recommendedVersion: 'stable',
|
||||
ports: {
|
||||
main: 3000
|
||||
}
|
||||
},
|
||||
labels: ['fider', 'feedback', 'suggestions']
|
||||
},
|
||||
{
|
||||
name: 'appwrite',
|
||||
@ -176,7 +190,8 @@ export const supportedServiceTypesAndVersions = [
|
||||
recommendedVersion: '1.0',
|
||||
ports: {
|
||||
main: 80
|
||||
}
|
||||
},
|
||||
labels: ['appwrite', 'database', 'storage', 'api', 'serverless']
|
||||
},
|
||||
// {
|
||||
// name: 'moodle',
|
||||
@ -198,7 +213,8 @@ export const supportedServiceTypesAndVersions = [
|
||||
recommendedVersion: 'latest',
|
||||
ports: {
|
||||
main: 8000
|
||||
}
|
||||
},
|
||||
labels: ['glitchtip', 'error-reporting', 'error', 'sentry', 'bugsnag']
|
||||
},
|
||||
{
|
||||
name: 'searxng',
|
||||
@ -209,7 +225,8 @@ export const supportedServiceTypesAndVersions = [
|
||||
recommendedVersion: 'latest',
|
||||
ports: {
|
||||
main: 8080
|
||||
}
|
||||
},
|
||||
labels: ['searxng', 'search', 'search-engine']
|
||||
},
|
||||
{
|
||||
name: 'weblate',
|
||||
@ -220,7 +237,8 @@ export const supportedServiceTypesAndVersions = [
|
||||
recommendedVersion: 'latest',
|
||||
ports: {
|
||||
main: 8080
|
||||
}
|
||||
},
|
||||
labels: ['weblate', 'translation', 'localization']
|
||||
},
|
||||
// {
|
||||
// name: 'taiga',
|
||||
@ -242,7 +260,8 @@ export const supportedServiceTypesAndVersions = [
|
||||
recommendedVersion: 'latest',
|
||||
ports: {
|
||||
main: 3000
|
||||
}
|
||||
},
|
||||
labels: ['grafana', 'monitoring', 'metrics', 'dashboard']
|
||||
},
|
||||
{
|
||||
name: 'trilium',
|
||||
@ -253,6 +272,7 @@ export const supportedServiceTypesAndVersions = [
|
||||
recommendedVersion: 'latest',
|
||||
ports: {
|
||||
main: 8080
|
||||
}
|
||||
},
|
||||
labels: ['trilium', 'notes', 'note-taking', 'wiki']
|
||||
},
|
||||
];
|
||||
|
@ -110,23 +110,64 @@ export async function getApplicationStatus(request: FastifyRequest<OnlyId>) {
|
||||
try {
|
||||
const { id } = request.params
|
||||
const { teamId } = request.user
|
||||
let payload = []
|
||||
const application: any = await getApplicationFromDB(id, teamId);
|
||||
if (application?.destinationDockerId) {
|
||||
if (application.buildPack === 'compose') {
|
||||
const { stdout: containers } = await executeDockerCmd({
|
||||
dockerId: application.destinationDocker.id,
|
||||
command:
|
||||
`docker ps -a --filter "label=coolify.applicationId=${id}" --format '{{json .}}'`
|
||||
});
|
||||
const containersArray = containers.trim().split('\n');
|
||||
if (containersArray.length > 0 && containersArray[0] !== '') {
|
||||
for (const container of containersArray) {
|
||||
let isRunning = false;
|
||||
let isExited = false;
|
||||
let isRestarting = false;
|
||||
const containerObj = JSON.parse(container);
|
||||
const status = containerObj.State
|
||||
if (status === 'running') {
|
||||
isRunning = true;
|
||||
}
|
||||
if (status === 'exited') {
|
||||
isExited = true;
|
||||
}
|
||||
if (status === 'restarting') {
|
||||
isRestarting = true;
|
||||
}
|
||||
payload.push({
|
||||
name: containerObj.Names,
|
||||
status: {
|
||||
isRunning,
|
||||
isExited,
|
||||
isRestarting
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let isRunning = false;
|
||||
let isExited = false;
|
||||
let isRestarting = false;
|
||||
const application: any = await getApplicationFromDB(id, teamId);
|
||||
if (application?.destinationDockerId) {
|
||||
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
|
||||
}
|
||||
}
|
||||
return {
|
||||
payload.push({
|
||||
name: id,
|
||||
status: {
|
||||
isRunning,
|
||||
isRestarting,
|
||||
isExited,
|
||||
};
|
||||
isRestarting
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
return payload
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
@ -289,13 +330,15 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
|
||||
baseImage,
|
||||
baseBuildImage,
|
||||
deploymentType,
|
||||
baseDatabaseBranch
|
||||
baseDatabaseBranch,
|
||||
dockerComposeFile,
|
||||
dockerComposeFileLocation,
|
||||
dockerComposeConfiguration
|
||||
} = request.body
|
||||
if (port) port = Number(port);
|
||||
if (exposePort) {
|
||||
exposePort = Number(exposePort);
|
||||
}
|
||||
|
||||
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 (denoOptions) denoOptions = denoOptions.trim();
|
||||
@ -324,6 +367,9 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
|
||||
baseImage,
|
||||
baseBuildImage,
|
||||
deploymentType,
|
||||
dockerComposeFile,
|
||||
dockerComposeFileLocation,
|
||||
dockerComposeConfiguration,
|
||||
...defaultConfiguration,
|
||||
connectedDatabase: { update: { hostedDatabaseDBName: baseDatabaseBranch } }
|
||||
}
|
||||
@ -342,6 +388,9 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
|
||||
baseImage,
|
||||
baseBuildImage,
|
||||
deploymentType,
|
||||
dockerComposeFile,
|
||||
dockerComposeFileLocation,
|
||||
dockerComposeConfiguration,
|
||||
...defaultConfiguration
|
||||
}
|
||||
});
|
||||
@ -506,6 +555,21 @@ export async function stopApplication(request: FastifyRequest<OnlyId>, reply: Fa
|
||||
const application: any = await getApplicationFromDB(id, teamId);
|
||||
if (application?.destinationDockerId) {
|
||||
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 });
|
||||
if (found) {
|
||||
await removeContainer({ id, dockerId: application.destinationDocker.id });
|
||||
@ -613,6 +677,24 @@ export async function getUsage(request) {
|
||||
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>) {
|
||||
try {
|
||||
const { id } = request.params
|
||||
@ -1159,7 +1241,7 @@ export async function getPreviews(request: FastifyRequest<OnlyId>) {
|
||||
|
||||
export async function getApplicationLogs(request: FastifyRequest<GetApplicationLogs>) {
|
||||
try {
|
||||
const { id } = request.params;
|
||||
const { id, containerId } = request.params;
|
||||
let { since = 0 } = request.query
|
||||
if (since !== 0) {
|
||||
since = day(since).unix();
|
||||
@ -1170,10 +1252,8 @@ export async function getApplicationLogs(request: FastifyRequest<GetApplicationL
|
||||
});
|
||||
if (destinationDockerId) {
|
||||
try {
|
||||
// const found = await checkContainer({ dockerId, container: id })
|
||||
// if (found) {
|
||||
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 stripLogsStderr = stderr.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
|
||||
const logs = stripLogsStderr.concat(stripLogsStdout)
|
||||
@ -1181,7 +1261,10 @@ export async function getApplicationLogs(request: FastifyRequest<GetApplicationL
|
||||
return { logs: sortedLogs }
|
||||
// }
|
||||
} catch (error) {
|
||||
const { statusCode } = error;
|
||||
const { statusCode, stderr } = error;
|
||||
if (stderr.startsWith('Error: No such container')) {
|
||||
return { logs: [], noContainer: true }
|
||||
}
|
||||
if (statusCode === 404) {
|
||||
return {
|
||||
logs: []
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { FastifyPluginAsync } from 'fastify';
|
||||
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';
|
||||
|
||||
@ -45,11 +45,13 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||
fastify.get<RestartPreviewApplication>('/:id/previews/:pullmergeRequestId/status', async (request) => await getPreviewStatus(request));
|
||||
fastify.post<RestartPreviewApplication>('/:id/previews/:pullmergeRequestId/restart', async (request, reply) => await restartPreview(request, reply));
|
||||
|
||||
fastify.get<GetApplicationLogs>('/:id/logs', async (request) => await getApplicationLogs(request));
|
||||
// fastify.get<GetApplicationLogs>('/:id/logs', async (request) => await getApplicationLogs(request));
|
||||
fastify.get<GetApplicationLogs>('/:id/logs/:containerId', async (request) => await getApplicationLogs(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('/: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<CancelDeployment>('/:id/cancel', async (request, reply) => await cancelDeployment(request, reply));
|
||||
|
@ -21,7 +21,10 @@ export interface SaveApplication extends OnlyId {
|
||||
baseImage: string,
|
||||
baseBuildImage: string,
|
||||
deploymentType: string,
|
||||
baseDatabaseBranch: string
|
||||
baseDatabaseBranch: string,
|
||||
dockerComposeFile: string,
|
||||
dockerComposeFileLocation: string,
|
||||
dockerComposeConfiguration: string
|
||||
}
|
||||
}
|
||||
export interface SaveApplicationSettings extends OnlyId {
|
||||
@ -84,7 +87,11 @@ export interface DeleteStorage extends OnlyId {
|
||||
path: string,
|
||||
}
|
||||
}
|
||||
export interface GetApplicationLogs extends OnlyId {
|
||||
export interface GetApplicationLogs {
|
||||
Params: {
|
||||
id: string,
|
||||
containerId: string
|
||||
}
|
||||
Querystring: {
|
||||
since: number,
|
||||
}
|
||||
|
@ -146,7 +146,7 @@ export async function showDashboard(request: FastifyRequest) {
|
||||
|
||||
let foundUnconfiguredApplication = false;
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -234,6 +234,8 @@ export async function traefikConfiguration(request, reply) {
|
||||
fqdn,
|
||||
id,
|
||||
port,
|
||||
buildPack,
|
||||
dockerComposeConfiguration,
|
||||
destinationDocker,
|
||||
destinationDockerId,
|
||||
settings: { previews, dualCerts, isCustomSSL }
|
||||
@ -241,6 +243,33 @@ export async function traefikConfiguration(request, reply) {
|
||||
if (destinationDockerId) {
|
||||
const { network, id: dockerId } = destinationDocker;
|
||||
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) {
|
||||
const domain = getDomain(fqdn);
|
||||
const nakedDomain = domain.replace(/^www\./, '');
|
||||
@ -604,13 +633,41 @@ export async function remoteTraefikConfiguration(request: FastifyRequest<OnlyId>
|
||||
fqdn,
|
||||
id,
|
||||
port,
|
||||
buildPack,
|
||||
dockerComposeConfiguration,
|
||||
destinationDocker,
|
||||
destinationDockerId,
|
||||
settings: { previews, dualCerts }
|
||||
settings: { previews, dualCerts, isCustomSSL }
|
||||
} = application;
|
||||
if (destinationDockerId) {
|
||||
const { id: dockerId, network } = destinationDocker;
|
||||
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) {
|
||||
const domain = getDomain(fqdn);
|
||||
const nakedDomain = domain.replace(/^www\./, '');
|
||||
@ -626,7 +683,8 @@ export async function remoteTraefikConfiguration(request: FastifyRequest<OnlyId>
|
||||
isRunning,
|
||||
isHttps,
|
||||
isWWW,
|
||||
isDualCerts: dualCerts
|
||||
isDualCerts: dualCerts,
|
||||
isCustomSSL
|
||||
});
|
||||
}
|
||||
if (previews) {
|
||||
@ -649,7 +707,8 @@ export async function remoteTraefikConfiguration(request: FastifyRequest<OnlyId>
|
||||
nakedDomain,
|
||||
isHttps,
|
||||
isWWW,
|
||||
isDualCerts: dualCerts
|
||||
isDualCerts: dualCerts,
|
||||
isCustomSSL
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -48,6 +48,7 @@
|
||||
"daisyui": "2.24.2",
|
||||
"dayjs": "1.11.5",
|
||||
"js-cookie": "3.0.1",
|
||||
"js-yaml": "4.1.0",
|
||||
"p-limit": "4.0.0",
|
||||
"svelte-file-dropzone": "^1.0.0",
|
||||
"svelte-select": "4.4.7",
|
||||
|
@ -110,7 +110,7 @@ async function send({
|
||||
if (
|
||||
response.status === 401 &&
|
||||
!path.startsWith('https://api.github') &&
|
||||
!path.includes('/v4/user')
|
||||
!path.includes('/v4/')
|
||||
) {
|
||||
Cookies.remove('token');
|
||||
}
|
||||
|
@ -12,6 +12,7 @@
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<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>
|
||||
|
@ -40,4 +40,6 @@
|
||||
<Icons.Laravel {isAbsolute} />
|
||||
{:else if application.buildPack?.toLowerCase() === 'heroku'}
|
||||
<Icons.Heroku {isAbsolute} />
|
||||
{:else if application.buildPack?.toLowerCase() === 'compose'}
|
||||
<Icons.Compose {isAbsolute} />
|
||||
{/if}
|
||||
|
@ -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"
|
||||
/>
|
@ -17,3 +17,4 @@ export { default as Eleventy } from './Eleventy.svelte';
|
||||
export { default as Deno } from './Deno.svelte';
|
||||
export { default as Laravel } from './Laravel.svelte';
|
||||
export { default as Heroku } from './Heroku.svelte';
|
||||
export { default as Compose } from './Compose.svelte';
|
||||
|
@ -56,6 +56,7 @@ export const isDeploymentEnabled: Writable<boolean> = writable(false);
|
||||
export function checkIfDeploymentEnabledApplications(isAdmin: boolean, application: any) {
|
||||
return (
|
||||
isAdmin &&
|
||||
(application.buildPack === 'compose') ||
|
||||
(application.fqdn || application.settings.isBot) &&
|
||||
application.gitSource &&
|
||||
application.repository &&
|
||||
@ -74,9 +75,8 @@ export function checkIfDeploymentEnabledServices(isAdmin: boolean, service: any)
|
||||
}
|
||||
export const status: Writable<any> = writable({
|
||||
application: {
|
||||
isRunning: false,
|
||||
isExited: false,
|
||||
isRestarting: false,
|
||||
statuses: [],
|
||||
overallStatus: 'stopped',
|
||||
loading: false,
|
||||
initialLoading: true
|
||||
},
|
||||
|
@ -29,7 +29,7 @@ export function findBuildPack(pack: string, packageManager = 'npm') {
|
||||
port: 80
|
||||
};
|
||||
}
|
||||
if (pack === 'docker') {
|
||||
if (pack === 'docker' || pack === 'compose') {
|
||||
return {
|
||||
...metaData,
|
||||
installCommand: null,
|
||||
@ -39,6 +39,7 @@ export function findBuildPack(pack: string, packageManager = 'npm') {
|
||||
port: null
|
||||
};
|
||||
}
|
||||
|
||||
if (pack === 'svelte') {
|
||||
return {
|
||||
...metaData,
|
||||
@ -235,6 +236,14 @@ export const buildPacks = [
|
||||
color: 'bg-sky-700',
|
||||
isCoolifyBuildPack: true,
|
||||
},
|
||||
{
|
||||
name: 'compose',
|
||||
type: 'base',
|
||||
fancyName: 'Docker Compose',
|
||||
hoverColor: 'hover:bg-sky-700',
|
||||
color: 'bg-sky-700',
|
||||
isCoolifyBuildPack: true,
|
||||
},
|
||||
{
|
||||
name: 'svelte',
|
||||
type: 'specific',
|
||||
|
@ -18,7 +18,7 @@
|
||||
|
||||
<div class="dropdown dropdown-bottom">
|
||||
<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
|
||||
class="h-6 w-6"
|
||||
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">
|
||||
<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
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
@ -58,7 +58,7 @@
|
||||
>
|
||||
</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
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
@ -75,7 +75,7 @@
|
||||
>
|
||||
</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
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
@ -94,7 +94,7 @@
|
||||
>
|
||||
</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
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
@ -116,7 +116,7 @@
|
||||
>
|
||||
</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
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
|
@ -160,12 +160,12 @@
|
||||
<span>Logs</span>
|
||||
</li>
|
||||
<li
|
||||
class:text-stone-600={!$status.application.isRunning}
|
||||
class:text-stone-600={$status.application.overallStatus === 'stopped'}
|
||||
class="rounded"
|
||||
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/logs`}
|
||||
>
|
||||
<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"
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -218,10 +218,10 @@
|
||||
</li>
|
||||
<li
|
||||
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`}
|
||||
>
|
||||
<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
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6"
|
||||
|
@ -59,7 +59,6 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { t } from '$lib/translations';
|
||||
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
|
||||
import {
|
||||
appSession,
|
||||
status,
|
||||
@ -140,13 +139,11 @@
|
||||
async function stopApplication() {
|
||||
try {
|
||||
$status.application.initialLoading = true;
|
||||
// $status.application.loading = true;
|
||||
await post(`/applications/${id}/stop`, {});
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
$status.application.initialLoading = false;
|
||||
// $status.application.loading = false;
|
||||
await getStatus();
|
||||
}
|
||||
}
|
||||
@ -154,18 +151,48 @@
|
||||
if ($status.application.loading) return;
|
||||
$status.application.loading = true;
|
||||
const data = await get(`/applications/${id}/status`);
|
||||
$status.application.isRunning = data.isRunning;
|
||||
$status.application.isExited = data.isExited;
|
||||
$status.application.isRestarting = data.isRestarting;
|
||||
|
||||
$status.application.statuses = data;
|
||||
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.initialLoading = false;
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
$status.application.initialLoading = true;
|
||||
$status.application.isRunning = false;
|
||||
$status.application.isExited = false;
|
||||
$status.application.isRestarting = false;
|
||||
$status.application.loading = false;
|
||||
$location = null;
|
||||
$isDeploymentEnabled = false;
|
||||
@ -173,15 +200,8 @@
|
||||
});
|
||||
onMount(async () => {
|
||||
setLocation(application, settings);
|
||||
$status.application.isRunning = false;
|
||||
$status.application.isExited = false;
|
||||
$status.application.isRestarting = false;
|
||||
$status.application.loading = false;
|
||||
if (
|
||||
application.gitSourceId &&
|
||||
application.destinationDockerId &&
|
||||
(application.fqdn || application.settings.isBot)
|
||||
) {
|
||||
if ($isDeploymentEnabled) {
|
||||
await getStatus();
|
||||
statusInterval = setInterval(async () => {
|
||||
await getStatus();
|
||||
@ -207,11 +227,16 @@
|
||||
<div class="flex justify-center items-center space-x-2">
|
||||
<div>Configurations</div>
|
||||
<div
|
||||
class="badge rounded uppercase"
|
||||
class:text-green-500={$status.application.isRunning}
|
||||
class:text-red-500={!$status.application.isRunning}
|
||||
class="badge badge-lg rounded uppercase"
|
||||
class:text-green-500={$status.application.overallStatus === 'healthy'}
|
||||
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>
|
||||
{/if}
|
||||
@ -245,16 +270,15 @@
|
||||
<div
|
||||
class="pt-4 flex flex-row items-start justify-center lg:justify-end space-x-2 order-1 lg:order-2"
|
||||
>
|
||||
{#if $status.application.isExited || $status.application.isRestarting}
|
||||
{#if $status.application.overallStatus === 'degraded' && application.buildPack !== 'compose'}
|
||||
<a
|
||||
id="applicationerror"
|
||||
href={$isDeploymentEnabled ? `/applications/${id}/logs` : null}
|
||||
class="icons bg-transparent text-sm text-error"
|
||||
class="btn btn-sm text-sm gap-2"
|
||||
sveltekit:prefetch
|
||||
>
|
||||
<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"
|
||||
stroke-width="1.5"
|
||||
stroke="currentcolor"
|
||||
@ -269,14 +293,14 @@
|
||||
<line x1="12" y1="8" x2="12" y2="12" />
|
||||
<line x1="12" y1="16" x2="12.01" y2="16" />
|
||||
</svg>
|
||||
Application Error
|
||||
</a>
|
||||
<Tooltip triggeredBy="#applicationerror">Application exited with an error!</Tooltip>
|
||||
{/if}
|
||||
{#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
|
||||
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"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
@ -292,18 +316,18 @@
|
||||
<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
|
||||
<line x1="11" y1="19.94" x2="11" y2="19.95" />
|
||||
</svg>
|
||||
Loading...
|
||||
</button>
|
||||
{:else if $status.application.isRunning}
|
||||
{:else if $status.application.overallStatus === 'healthy'}
|
||||
<button
|
||||
id="stop"
|
||||
on:click={stopApplication}
|
||||
type="submit"
|
||||
disabled={!$isDeploymentEnabled}
|
||||
class="icons bg-transparent text-error"
|
||||
class="btn btn-sm btn-error gap-2"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6"
|
||||
class="w-6 h-6 "
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
@ -314,16 +338,14 @@
|
||||
<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>
|
||||
</svg> Stop
|
||||
</button>
|
||||
<Tooltip triggeredBy="#stop">Stop</Tooltip>
|
||||
|
||||
{#if application.buildPack !== 'compose'}
|
||||
<button
|
||||
id="restart"
|
||||
on:click={restartApplication}
|
||||
type="submit"
|
||||
disabled={!$isDeploymentEnabled}
|
||||
class="icons bg-transparent"
|
||||
class="btn btn-sm gap-2"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -338,14 +360,12 @@
|
||||
<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>
|
||||
</svg> Restart
|
||||
</button>
|
||||
<Tooltip triggeredBy="#restart">Restart (useful to change secrets)</Tooltip>
|
||||
|
||||
{/if}
|
||||
<button
|
||||
id="forceredeploy"
|
||||
disabled={!$isDeploymentEnabled}
|
||||
class="icons bg-transparent "
|
||||
class="btn btn-sm gap-2"
|
||||
on:click={() => handleDeploySubmit(true)}
|
||||
>
|
||||
<svg
|
||||
@ -364,17 +384,43 @@
|
||||
transform="rotate(-45 12 12)"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
Force Redeploy
|
||||
</button>
|
||||
<Tooltip triggeredBy="#forceredeploy">Force Redeploy (without cache)</Tooltip>
|
||||
{:else if $isDeploymentEnabled && !$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
|
||||
{#if $status.application.overallStatus === 'degraded'}
|
||||
<button
|
||||
class="icons flex items-center font-bold"
|
||||
on:click={stopApplication}
|
||||
type="submit"
|
||||
disabled={!$isDeploymentEnabled}
|
||||
on:click={() => handleDeploySubmit(false)}
|
||||
class="btn btn-sm btn-error gap-2"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6 mr-2 text-green-500"
|
||||
class="w-6 h-6 "
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<rect x="6" y="5" width="4" height="14" rx="1" />
|
||||
<rect x="14" y="5" width="4" height="14" rx="1" />
|
||||
</svg> Stop
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
class="btn btn-sm gap-2"
|
||||
class:btn-primary={$status.application.overallStatus !== 'degraded'}
|
||||
disabled={!$isDeploymentEnabled}
|
||||
on:click={() => handleDeploySubmit(false)}
|
||||
>
|
||||
{#if $status.application.overallStatus !== 'degraded'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
@ -385,12 +431,33 @@
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M7 4v16l13 -8z" />
|
||||
</svg>
|
||||
Deploy
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M16.3 5h.7a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h5l-2.82 -2.82m0 5.64l2.82 -2.82"
|
||||
transform="rotate(-45 12 12)"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
{$status.application.overallStatus === 'degraded'
|
||||
? $status.application.statuses.length === 1
|
||||
? 'Force Redeploy'
|
||||
: 'Redeploy Stack'
|
||||
: 'Deploy'}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if $location && $status.application.isRunning}
|
||||
<a id="openApplication" href={$location} target="_blank" class="icons bg-transparent "
|
||||
{#if $location && $status.application.overallStatus === 'healthy'}
|
||||
<a href={$location} target="_blank" class="btn btn-sm gap-2 text-sm bg-primary"
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
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" />
|
||||
<line x1="10" y1="14" x2="20" y2="4" />
|
||||
<polyline points="15 4 20 4 20 9" />
|
||||
</svg></a
|
||||
</svg>Open</a
|
||||
>
|
||||
<Tooltip triggeredBy="#openApplication">Open Application</Tooltip>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -14,6 +14,9 @@
|
||||
export let foundConfig: any;
|
||||
export let scanning: 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) {
|
||||
try {
|
||||
@ -25,10 +28,20 @@
|
||||
delete tempBuildPack.fancyName;
|
||||
delete tempBuildPack.color;
|
||||
delete tempBuildPack.hoverColor;
|
||||
|
||||
if (foundConfig?.buildPack !== name) {
|
||||
await post(`/applications/${id}`, { ...tempBuildPack, buildPack: name });
|
||||
let composeConfiguration: any = {}
|
||||
if (!dockerComposeConfiguration && dockerComposeFile) {
|
||||
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 });
|
||||
return await goto(from || `/applications/${id}`);
|
||||
} catch (error) {
|
||||
|
@ -95,7 +95,7 @@
|
||||
if (newWindow?.closed) {
|
||||
clearInterval(timer);
|
||||
$appSession.tokens.gitlab = localStorage.getItem('gitLabToken');
|
||||
localStorage.removeItem('gitLabToken');
|
||||
// localStorage.removeItem('gitLabToken');
|
||||
resolve();
|
||||
}
|
||||
}, 100);
|
||||
|
@ -165,7 +165,7 @@
|
||||
placeholder="eg: https://github.com/coollabsio/nodejs-example/tree/main"
|
||||
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
|
||||
</button>
|
||||
</div>
|
||||
|
@ -12,6 +12,7 @@
|
||||
const response = await get(`/applications/${params.id}/configuration/buildpack`);
|
||||
return {
|
||||
props: {
|
||||
application,
|
||||
...response
|
||||
}
|
||||
};
|
||||
@ -25,22 +26,6 @@
|
||||
</script>
|
||||
|
||||
<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 projectId: any;
|
||||
export let repository: any;
|
||||
@ -49,6 +34,28 @@
|
||||
export let application: any;
|
||||
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 }) {
|
||||
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 {
|
||||
if (type === 'gitlab') {
|
||||
const files = await get(`${apiUrl}/v4/projects/${projectId}/repository/tree`, {
|
||||
const headers = isPublicRepository
|
||||
? {}
|
||||
: {
|
||||
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(
|
||||
(file: { name: string; type: string }) =>
|
||||
@ -82,6 +123,14 @@
|
||||
(file: { name: string; type: string }) =>
|
||||
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(
|
||||
(file: { name: string; type: string }) =>
|
||||
file.name === 'Cargo.toml' && file.type === 'blob'
|
||||
@ -105,11 +154,24 @@
|
||||
const laravel = files.find(
|
||||
(file: { name: string; type: string }) => file.name === 'artisan' && file.type === 'blob'
|
||||
);
|
||||
|
||||
if (yarnLock) packageManager = 'yarn';
|
||||
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);
|
||||
} else if (packageJson && !laravel) {
|
||||
const path = packageJson.path;
|
||||
@ -135,8 +197,13 @@
|
||||
foundConfig = findBuildPack('node', packageManager);
|
||||
}
|
||||
} else if (type === 'github') {
|
||||
const headers = isPublicRepository
|
||||
? {}
|
||||
: {
|
||||
Authorization: `token ${$appSession.tokens.github}`
|
||||
};
|
||||
const files = await get(`${apiUrl}/repos/${repository}/contents?ref=${branch}`, {
|
||||
Authorization: `Bearer ${$appSession.tokens.github}`,
|
||||
...headers,
|
||||
Accept: 'application/vnd.github.v2.json'
|
||||
});
|
||||
const packageJson = files.find(
|
||||
@ -155,6 +222,14 @@
|
||||
(file: { name: string; type: string }) =>
|
||||
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(
|
||||
(file: { name: string; type: string }) =>
|
||||
file.name === 'Cargo.toml' && file.type === 'file'
|
||||
@ -182,11 +257,30 @@
|
||||
if (yarnLock) packageManager = 'yarn';
|
||||
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);
|
||||
} else if (packageJson && !laravel) {
|
||||
const data: any = await get(`${packageJson.git_url}`, {
|
||||
Authorization: `Bearer ${$appSession.tokens.github}`,
|
||||
...headers,
|
||||
Accept: 'application/vnd.github.v2.raw'
|
||||
});
|
||||
const json = JSON.parse(data) || {};
|
||||
@ -214,30 +308,39 @@
|
||||
error.message === '401 Unauthorized'
|
||||
) {
|
||||
if (application.gitSource.gitlabAppId) {
|
||||
let htmlUrl = application.gitSource.htmlUrl;
|
||||
const left = screen.width / 2 - 1020 / 2;
|
||||
const top = screen.height / 2 - 618 / 2;
|
||||
const newWindow = open(
|
||||
`${htmlUrl}/oauth/authorize?client_id=${application.gitSource.gitlabApp.appId}&redirect_uri=${window.location.origin}/webhooks/gitlab&response_type=code&scope=api+email+read_repository&state=${$page.params.id}`,
|
||||
'GitLab',
|
||||
'resizable=1, scrollbars=1, fullscreen=0, height=618, width=1020,top=' +
|
||||
top +
|
||||
', left=' +
|
||||
left +
|
||||
', toolbar=0, menubar=0, status=0'
|
||||
);
|
||||
const timer = setInterval(() => {
|
||||
if (newWindow?.closed) {
|
||||
clearInterval(timer);
|
||||
window.location.reload();
|
||||
if (!$appSession.tokens.gitlab) {
|
||||
await getGitlabToken();
|
||||
}
|
||||
}, 100);
|
||||
scanRepository(isPublicRepository);
|
||||
// let htmlUrl = application.gitSource.htmlUrl;
|
||||
// const left = screen.width / 2 - 1020 / 2;
|
||||
// const top = screen.height / 2 - 618 / 2;
|
||||
// const newWindow = open(
|
||||
// `${htmlUrl}/oauth/authorize?client_id=${
|
||||
// application.gitSource.gitlabApp.appId
|
||||
// }&redirect_uri=${getAPIUrl()}/webhooks/gitlab&response_type=code&scope=api+email+read_repository&state=${
|
||||
// $page.params.id
|
||||
// }`,
|
||||
// 'GitLab',
|
||||
// 'resizable=1, scrollbars=1, fullscreen=0, height=618, width=1020,top=' +
|
||||
// top +
|
||||
// ', left=' +
|
||||
// left +
|
||||
// ', toolbar=0, menubar=0, status=0'
|
||||
// );
|
||||
// const timer = setInterval(() => {
|
||||
// if (newWindow?.closed) {
|
||||
// clearInterval(timer);
|
||||
// $appSession.tokens.gitlab = localStorage.getItem('gitLabToken');
|
||||
// // localStorage.removeItem('gitLabToken' );
|
||||
|
||||
// }
|
||||
// }, 100);
|
||||
}
|
||||
}
|
||||
if (error.message === 'Bad credentials') {
|
||||
} else if (error.message === 'Bad credentials') {
|
||||
const { token } = await get(`/applications/${id}/configuration/githubToken`);
|
||||
$appSession.tokens.github = token;
|
||||
return await scanRepository();
|
||||
return await scanRepository(isPublicRepository);
|
||||
}
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
@ -246,11 +349,7 @@
|
||||
}
|
||||
}
|
||||
onMount(async () => {
|
||||
if (!isPublicRepository) {
|
||||
await scanRepository();
|
||||
} else {
|
||||
scanning = false;
|
||||
}
|
||||
await scanRepository(isPublicRepository);
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -274,9 +373,17 @@
|
||||
<div class="max-w-screen-2xl mx-auto px-10">
|
||||
<div class="title pb-2">Coolify Base</div>
|
||||
<div class="flex flex-wrap justify-center">
|
||||
{#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type ==='base') as buildPack}
|
||||
{#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type === 'base') as buildPack}
|
||||
<div class="p-2">
|
||||
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
|
||||
<BuildPack
|
||||
{packageManager}
|
||||
{buildPack}
|
||||
{scanning}
|
||||
bind:foundConfig
|
||||
{dockerComposeFile}
|
||||
{dockerComposeFileLocation}
|
||||
{dockerComposeConfiguration}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@ -284,7 +391,7 @@
|
||||
<div class="max-w-screen-2xl mx-auto px-10">
|
||||
<div class="title pb-2">Coolify Specific</div>
|
||||
<div class="flex flex-wrap justify-center">
|
||||
{#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type ==='specific') as buildPack}
|
||||
{#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type === 'specific') as buildPack}
|
||||
<div class="p-2">
|
||||
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
|
||||
</div>
|
||||
|
@ -28,10 +28,12 @@
|
||||
<script lang="ts">
|
||||
export let application: any;
|
||||
export let settings: any;
|
||||
|
||||
import yaml from 'js-yaml';
|
||||
import { page } from '$app/stores';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import Select from 'svelte-select';
|
||||
import { get, post } from '$lib/api';
|
||||
import { get, getAPIUrl, post } from '$lib/api';
|
||||
import cuid from 'cuid';
|
||||
import {
|
||||
addToast,
|
||||
@ -45,19 +47,27 @@
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common';
|
||||
import Setting from '$lib/components/Setting.svelte';
|
||||
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { fade } from 'svelte/transition';
|
||||
import Beta from '$lib/components/Beta.svelte';
|
||||
|
||||
const { id } = $page.params;
|
||||
|
||||
$: isDisabled =
|
||||
!$appSession.isAdmin || $status.application.isRunning || $status.application.initialLoading;
|
||||
!$appSession.isAdmin ||
|
||||
$status.application.overallStatus === 'degraded' ||
|
||||
$status.application.overallStatus === 'healthy' ||
|
||||
$status.application.initialLoading;
|
||||
|
||||
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
|
||||
let statues: any = {};
|
||||
let loading = false;
|
||||
let fqdnEl: any = null;
|
||||
let forceSave = false;
|
||||
let isPublicRepository = application.settings.isPublicRepository;
|
||||
let apiUrl = application.gitSource.apiUrl;
|
||||
let branch = application.branch;
|
||||
let repository = application.repository;
|
||||
let debug = application.settings.debug;
|
||||
let previews = application.settings.previews;
|
||||
let dualCerts = application.settings.dualCerts;
|
||||
@ -65,6 +75,12 @@
|
||||
let autodeploy = application.settings.autodeploy;
|
||||
let isBot = application.settings.isBot;
|
||||
let isDBBranching = application.settings.isDBBranching;
|
||||
let htmlUrl = application.gitSource.htmlUrl;
|
||||
|
||||
let dockerComposeFile = JSON.parse(application.dockerComposeFile) || null;
|
||||
let dockerComposeServices: any[] = [];
|
||||
let dockerComposeFileLocation = application.dockerComposeFileLocation;
|
||||
let dockerComposeConfiguration = JSON.parse(application.dockerComposeConfiguration) || {};
|
||||
|
||||
let baseDatabaseBranch: any = application?.connectedDatabase?.hostedDatabaseDBName || null;
|
||||
let nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
|
||||
@ -86,6 +102,26 @@
|
||||
label: 'Uvicorn'
|
||||
}
|
||||
];
|
||||
|
||||
function normalizeDockerServices(services: any[]) {
|
||||
const tempdockerComposeServices = [];
|
||||
for (const [name, data] of Object.entries(services)) {
|
||||
tempdockerComposeServices.push({
|
||||
name,
|
||||
data
|
||||
});
|
||||
}
|
||||
for (const service of tempdockerComposeServices) {
|
||||
if (!dockerComposeConfiguration[service.name]) {
|
||||
dockerComposeConfiguration[service.name] = {};
|
||||
}
|
||||
}
|
||||
return tempdockerComposeServices;
|
||||
}
|
||||
if (dockerComposeFile?.services) {
|
||||
dockerComposeServices = normalizeDockerServices(dockerComposeFile.services);
|
||||
}
|
||||
|
||||
function containerClass() {
|
||||
return 'text-white bg-transparent font-thin px-0 w-full border border-dashed border-coolgray-200';
|
||||
}
|
||||
@ -148,7 +184,7 @@
|
||||
isCustomSSL = !isCustomSSL;
|
||||
}
|
||||
if (name === 'isBot') {
|
||||
if ($status.application.isRunning) return;
|
||||
if ($status.application.overallStatus !== 'stopped') return;
|
||||
isBot = !isBot;
|
||||
application.settings.isBot = isBot;
|
||||
application.fqdn = null;
|
||||
@ -200,26 +236,42 @@
|
||||
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
|
||||
}
|
||||
}
|
||||
async function handleSubmit() {
|
||||
async function handleSubmit(toast: boolean = true) {
|
||||
if (loading) return;
|
||||
loading = true;
|
||||
if (toast) loading = true;
|
||||
try {
|
||||
nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
|
||||
if (application.deploymentType)
|
||||
application.deploymentType = application.deploymentType.toLowerCase();
|
||||
!isBot &&
|
||||
(await post(`/applications/${id}/check`, {
|
||||
if (!isBot) {
|
||||
await post(`/applications/${id}/check`, {
|
||||
fqdn: application.fqdn,
|
||||
forceSave,
|
||||
dualCerts,
|
||||
exposePort: application.exposePort
|
||||
}));
|
||||
await post(`/applications/${id}`, { ...application, baseDatabaseBranch });
|
||||
});
|
||||
for (const service of dockerComposeServices) {
|
||||
if (dockerComposeConfiguration[service.name].fqdn) {
|
||||
await post(`/applications/${id}/check`, {
|
||||
fqdn: dockerComposeConfiguration[service.name].fqdn,
|
||||
forceSave,
|
||||
dualCerts,
|
||||
exposePort: application.exposePort
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
await post(`/applications/${id}`, {
|
||||
...application,
|
||||
baseDatabaseBranch,
|
||||
dockerComposeConfiguration: JSON.stringify(dockerComposeConfiguration)
|
||||
});
|
||||
setLocation(application, settings);
|
||||
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
|
||||
|
||||
forceSave = false;
|
||||
|
||||
toast &&
|
||||
addToast({
|
||||
message: 'Configuration saved.',
|
||||
type: 'success'
|
||||
@ -281,10 +333,130 @@
|
||||
return false;
|
||||
}
|
||||
}
|
||||
async function getGitlabToken() {
|
||||
return await new Promise<void>((resolve, reject) => {
|
||||
const left = screen.width / 2 - 1020 / 2;
|
||||
const top = screen.height / 2 - 618 / 2;
|
||||
const newWindow = open(
|
||||
`${htmlUrl}/oauth/authorize?client_id=${
|
||||
application.gitSource.gitlabApp.appId
|
||||
}&redirect_uri=${getAPIUrl()}/webhooks/gitlab&response_type=code&scope=api+email+read_repository&state=${
|
||||
$page.params.id
|
||||
}`,
|
||||
'GitLab',
|
||||
'resizable=1, scrollbars=1, fullscreen=0, height=618, width=1020,top=' +
|
||||
top +
|
||||
', left=' +
|
||||
left +
|
||||
', toolbar=0, menubar=0, status=0'
|
||||
);
|
||||
const timer = setInterval(() => {
|
||||
if (newWindow?.closed) {
|
||||
clearInterval(timer);
|
||||
$appSession.tokens.gitlab = localStorage.getItem('gitLabToken');
|
||||
localStorage.removeItem('gitLabToken');
|
||||
resolve();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
async function reloadCompose() {
|
||||
try {
|
||||
if (application.gitSource.type === 'github') {
|
||||
const headers = isPublicRepository
|
||||
? {}
|
||||
: {
|
||||
Authorization: `token ${$appSession.tokens.github}`
|
||||
};
|
||||
const data = await get(
|
||||
`${apiUrl}/repos/${repository}/contents/${dockerComposeFileLocation}?ref=${branch}`,
|
||||
{
|
||||
...headers,
|
||||
Accept: 'application/vnd.github.v2.json'
|
||||
}
|
||||
);
|
||||
if (data?.content) {
|
||||
const content = atob(data.content);
|
||||
let dockerComposeFileContent = JSON.stringify(yaml.load(content) || null);
|
||||
let dockerComposeFileContentJSON = JSON.parse(dockerComposeFileContent);
|
||||
dockerComposeServices = normalizeDockerServices(dockerComposeFileContentJSON?.services);
|
||||
application.dockerComposeFile = dockerComposeFileContent;
|
||||
await handleSubmit(false);
|
||||
}
|
||||
}
|
||||
if (application.gitSource.type === 'gitlab') {
|
||||
if (!$appSession.tokens.gitlab) {
|
||||
await getGitlabToken();
|
||||
}
|
||||
const headers = isPublicRepository
|
||||
? {}
|
||||
: {
|
||||
Authorization: `Bearer ${$appSession.tokens.gitlab}`
|
||||
};
|
||||
const url = isPublicRepository
|
||||
? ``
|
||||
: `/v4/projects/${application.projectId}/repository/tree`;
|
||||
const files = await get(`${apiUrl}${url}`, {
|
||||
...headers
|
||||
});
|
||||
const dockerComposeFileYml = files.find(
|
||||
(file: { name: string; type: string }) =>
|
||||
file.name === dockerComposeFileLocation && file.type === 'blob'
|
||||
);
|
||||
const id = dockerComposeFileYml.id;
|
||||
|
||||
const data = await get(
|
||||
`${apiUrl}/v4/projects/${application.projectId}/repository/blobs/${id}`,
|
||||
{
|
||||
...headers
|
||||
}
|
||||
);
|
||||
if (data?.content) {
|
||||
const content = atob(data.content);
|
||||
let dockerComposeFileContent = JSON.stringify(yaml.load(content) || null);
|
||||
let dockerComposeFileContentJSON = JSON.parse(dockerComposeFileContent);
|
||||
dockerComposeServices = normalizeDockerServices(dockerComposeFileContentJSON?.services);
|
||||
application.dockerComposeFile = dockerComposeFileContent;
|
||||
await handleSubmit(false);
|
||||
}
|
||||
}
|
||||
|
||||
addToast({
|
||||
message: 'Compose file reloaded.',
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
errorNotification(error);
|
||||
}
|
||||
}
|
||||
$: if ($status.application.statuses) {
|
||||
for (const service of dockerComposeServices) {
|
||||
getStatus(service);
|
||||
}
|
||||
}
|
||||
function getStatus(service: any) {
|
||||
let foundStatus = null;
|
||||
const foundService = $status.application.statuses.find(
|
||||
(s: any) => s.name === `${application.id}-${service.name}`
|
||||
);
|
||||
if (foundService) {
|
||||
const statusText = foundService?.status;
|
||||
if (statusText?.isRunning) {
|
||||
foundStatus = 'Running';
|
||||
}
|
||||
if (statusText?.isExited) {
|
||||
foundStatus = 'Exited';
|
||||
}
|
||||
if (statusText?.isRestarting) {
|
||||
foundStatus = 'Restarting';
|
||||
}
|
||||
}
|
||||
statues[service.name] = foundStatus || 'Stopped';
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-full">
|
||||
<form on:submit|preventDefault={handleSubmit}>
|
||||
<form on:submit|preventDefault={() => handleSubmit()}>
|
||||
<div class="mx-auto w-full">
|
||||
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
|
||||
<div class="title font-bold pb-3 ">General</div>
|
||||
@ -345,9 +517,9 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="buildPack">{$t('application.build_pack')}</label>
|
||||
<label for="buildPack">{$t('application.build_pack')} </label>
|
||||
{#if isDisabled}
|
||||
<input class="uppercase w-full" disabled={isDisabled} value={application.buildPack} />
|
||||
<input class="capitalize w-full" disabled={isDisabled} value={application.buildPack} />
|
||||
{:else}
|
||||
<a
|
||||
href={`/applications/${id}/configuration/buildpack?from=/applications/${id}`}
|
||||
@ -372,6 +544,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#if application.buildPack !== 'compose'}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
id="isBot"
|
||||
@ -380,10 +553,11 @@
|
||||
on:click={() => changeSettings('isBot')}
|
||||
title="Is your application a bot?"
|
||||
description="You can deploy applications without domains or make them to listen on the <span class='text-settings font-bold'>Exposed Port</span>.<br></Setting><br>Useful to host <span class='text-settings font-bold'>Twitch bots, regular jobs, or anything that does not require an incoming HTTP connection.</span>"
|
||||
disabled={$status.application.isRunning}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
</div>
|
||||
{#if !isBot}
|
||||
{/if}
|
||||
{#if !isBot && application.buildPack !== 'compose'}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="fqdn"
|
||||
>{$t('application.url_fqdn')}
|
||||
@ -446,15 +620,15 @@
|
||||
<Setting
|
||||
id="dualCerts"
|
||||
dataTooltip={$t('forms.must_be_stopped_to_modify')}
|
||||
disabled={$status.application.isRunning}
|
||||
disabled={isDisabled}
|
||||
isCenter={false}
|
||||
bind:setting={dualCerts}
|
||||
title={$t('application.ssl_www_and_non_www')}
|
||||
description="Generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-settings'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both."
|
||||
on:click={() => !$status.application.isRunning && changeSettings('dualCerts')}
|
||||
on:click={() => !isDisabled && changeSettings('dualCerts')}
|
||||
/>
|
||||
</div>
|
||||
{#if isHttps}
|
||||
{#if isHttps && application.buildPack !== 'compose'}
|
||||
<div class="grid grid-cols-2 items-center pb-4">
|
||||
<Setting
|
||||
id="isCustomSSL"
|
||||
@ -468,8 +642,10 @@
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="title font-bold pb-3 pt-10 border-b border-coolgray-500 mb-6">Build & Deploy</div>
|
||||
{#if application.buildPack !== 'compose'}
|
||||
<div class="title font-bold pb-3 pt-10 border-b border-coolgray-500 mb-6">
|
||||
Build & Deploy
|
||||
</div>
|
||||
<div class="grid grid-flow-row gap-2 px-4 pr-5">
|
||||
{#if application.buildCommand || application.buildPack === 'rust' || application.buildPack === 'laravel'}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
@ -486,7 +662,7 @@
|
||||
{isDisabled}
|
||||
containerClasses={isDisabled && containerClass()}
|
||||
id="baseBuildImages"
|
||||
showIndicator={!$status.application.isRunning}
|
||||
showIndicator={!isDisabled}
|
||||
items={application.baseBuildImages}
|
||||
on:select={selectBaseBuildImage}
|
||||
value={application.baseBuildImage}
|
||||
@ -506,7 +682,7 @@
|
||||
{isDisabled}
|
||||
containerClasses={isDisabled && containerClass()}
|
||||
id="baseImages"
|
||||
showIndicator={!$status.application.isRunning}
|
||||
showIndicator={!isDisabled}
|
||||
items={application.baseImages}
|
||||
on:select={selectBaseImage}
|
||||
value={application.baseImage}
|
||||
@ -528,7 +704,7 @@
|
||||
{isDisabled}
|
||||
containerClasses={isDisabled && containerClass()}
|
||||
id="deploymentTypes"
|
||||
showIndicator={!$status.application.isRunning}
|
||||
showIndicator={!isDisabled}
|
||||
items={['static', 'node']}
|
||||
on:select={selectDeploymentType}
|
||||
value={application.deploymentType}
|
||||
@ -639,7 +815,9 @@
|
||||
<div class="grid grid-cols-2 items-center pt-4">
|
||||
<label for="port"
|
||||
>{$t('forms.port')}
|
||||
<Explainer explanation={'The port your application listens on.'} /></label
|
||||
<Explainer
|
||||
explanation={'The port your application listens inside the docker container.'}
|
||||
/></label
|
||||
>
|
||||
<input
|
||||
class="w-full"
|
||||
@ -660,7 +838,7 @@
|
||||
>
|
||||
<input
|
||||
class="w-full"
|
||||
readonly={!$appSession.isAdmin && !$status.application.isRunning}
|
||||
readonly={!isDisabled}
|
||||
disabled={isDisabled}
|
||||
name="exposePort"
|
||||
id="exposePort"
|
||||
@ -811,6 +989,98 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="title font-bold pb-3 pt-10 border-b border-coolgray-500 mb-6">
|
||||
Stack <Beta />
|
||||
{#if $appSession.isAdmin}
|
||||
<button class="btn btn-sm btn-primary" on:click|preventDefault={reloadCompose}
|
||||
>Reload Docker Compose File</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grid grid-flow-row gap-2">
|
||||
{#each dockerComposeServices as service}
|
||||
<div
|
||||
class="grid items-center bg-coolgray-100 rounded border border-coolgray-300 p-2 px-4"
|
||||
>
|
||||
<div class="text-xl font-bold uppercase">
|
||||
{service.name}
|
||||
<span
|
||||
class="badge rounded text-white"
|
||||
class:text-red-500={statues[service.name] === 'Exited' ||
|
||||
statues[service.name] === 'Stopped'}
|
||||
class:text-yellow-400={statues[service.name] === 'Restarting'}
|
||||
class:text-green-500={statues[service.name] === 'Running'}
|
||||
>{statues[service.name] || 'Loading...'}</span
|
||||
>
|
||||
</div>
|
||||
<div class="text-xs">{application.id}-{service.name}</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-8">
|
||||
<label for="fqdn"
|
||||
>{$t('application.url_fqdn')}
|
||||
<Explainer
|
||||
explanation={"If you specify <span class='text-settings font-bold'>https</span>, the application will be accessible only over https.<br>SSL certificate will be generated automatically.<br><br>If you specify <span class='text-settings font-bold'>www</span>, the application will be redirected (302) from non-www and vice versa.<br><br>To modify the domain, you must first stop the application.<br><br><span class='text-settings font-bold'>You must set your DNS to point to the server IP in advance.</span>"}
|
||||
/>
|
||||
</label>
|
||||
<div>
|
||||
<input
|
||||
class="w-full"
|
||||
disabled={isDisabled}
|
||||
readonly={!$appSession.isAdmin}
|
||||
name="fqdn"
|
||||
id="fqdn"
|
||||
bind:value={dockerComposeConfiguration[service.name].fqdn}
|
||||
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
|
||||
placeholder="eg: https://coollabs.io"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-8">
|
||||
<label for="destinationdns"
|
||||
>Internal DNS on the deployed Destination
|
||||
<Explainer
|
||||
explanation={'You can use these DNS names to access the application from other resources in your Destination.'}
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
for="destinationdns"
|
||||
class="w-full"
|
||||
disabled
|
||||
readonly
|
||||
value={`${application.id}-${service.name}`}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-8">
|
||||
<label for="stackdns"
|
||||
>Internal DNS in the current stack
|
||||
<Explainer
|
||||
explanation={'You can use these DNS names to access the application from this stack.'}
|
||||
/>
|
||||
</label>
|
||||
<input for="stackdns" class="w-full" disabled readonly value={service.name} />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-8 pb-4">
|
||||
<label for="port"
|
||||
>{$t('forms.port')}
|
||||
<Explainer
|
||||
explanation={'The port your application listens inside the docker container.'}
|
||||
/></label
|
||||
>
|
||||
<input
|
||||
class="w-full"
|
||||
disabled={isDisabled}
|
||||
readonly={!$appSession.isAdmin}
|
||||
name="port"
|
||||
id="port"
|
||||
required={!!dockerComposeConfiguration[service.name].fqdn}
|
||||
bind:value={dockerComposeConfiguration[service.name].port}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -3,11 +3,9 @@
|
||||
import { get } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import LoadingLogs from '$lib/components/LoadingLogs.svelte';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||
import { status } from '$lib/store';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let application: any = {};
|
||||
let logsLoading = false;
|
||||
let loadLogsInterval: any = null;
|
||||
@ -17,47 +15,52 @@
|
||||
let followingLogs: any;
|
||||
let logsEl: any;
|
||||
let position = 0;
|
||||
if (
|
||||
!$status.application.isExited &&
|
||||
!$status.application.isRestarting &&
|
||||
!$status.application.isRunning
|
||||
) {
|
||||
goto(`/applications/${$page.params.id}/`, { replaceState: true });
|
||||
}
|
||||
let services: any = [];
|
||||
let selectedService: any = null;
|
||||
let noContainer = false;
|
||||
|
||||
const { id } = $page.params;
|
||||
onMount(async () => {
|
||||
const response = await get(`/applications/${id}`);
|
||||
application = response.application;
|
||||
loadAllLogs();
|
||||
loadLogsInterval = setInterval(() => {
|
||||
loadLogs();
|
||||
}, 1000);
|
||||
if (response.application.dockerComposeFile) {
|
||||
services = normalizeDockerServices(
|
||||
JSON.parse(response.application.dockerComposeFile).services
|
||||
);
|
||||
} else {
|
||||
services = [
|
||||
{
|
||||
name: ''
|
||||
}
|
||||
];
|
||||
await selectService('');
|
||||
}
|
||||
});
|
||||
onDestroy(() => {
|
||||
clearInterval(loadLogsInterval);
|
||||
clearInterval(followingInterval);
|
||||
});
|
||||
async function loadAllLogs() {
|
||||
try {
|
||||
logsLoading = true;
|
||||
const data: any = await get(`/applications/${id}/logs`);
|
||||
if (data?.logs) {
|
||||
lastLog = data.logs[data.logs.length - 1];
|
||||
logs = data.logs;
|
||||
}
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
logsLoading = false;
|
||||
function normalizeDockerServices(services: any[]) {
|
||||
const tempdockerComposeServices = [];
|
||||
for (const [name, data] of Object.entries(services)) {
|
||||
tempdockerComposeServices.push({
|
||||
name,
|
||||
data
|
||||
});
|
||||
}
|
||||
return tempdockerComposeServices;
|
||||
}
|
||||
async function loadLogs() {
|
||||
if (logsLoading) return;
|
||||
try {
|
||||
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]) {
|
||||
logs = logs.concat(newLogs.logs);
|
||||
lastLog = newLogs.logs[newLogs.logs.length - 1];
|
||||
@ -89,6 +92,22 @@
|
||||
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>
|
||||
|
||||
<div class="mx-auto w-full">
|
||||
@ -96,9 +115,27 @@
|
||||
<div class="title font-bold pb-3">Application Logs</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row justify-center space-x-2">
|
||||
<div class="flex gap-2 lg:gap-8 pb-4">
|
||||
{#each services as service}
|
||||
<button
|
||||
on:click={() => selectService(service, true)}
|
||||
class:bg-primary={selectedService ===
|
||||
`${application.id}${service.name ? `-${service.name}` : ''}`}
|
||||
class:bg-coolgray-200={selectedService !==
|
||||
`${application.id}${service.name ? `-${service.name}` : ''}`}
|
||||
class="w-full rounded p-5 hover:bg-primary font-bold"
|
||||
>
|
||||
{application.id}{service.name ? `-${service.name}` : ''}</button
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if selectedService}
|
||||
<div class="flex flex-row justify-center space-x-2">
|
||||
{#if logs.length === 0}
|
||||
<div class="text-xl font-bold tracking-tighter">{$t('application.build.waiting_logs')}</div>
|
||||
{#if noContainer}
|
||||
<div class="text-xl font-bold tracking-tighter">Container not found / exited.</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="relative w-full">
|
||||
<div class="flex justify-start sticky space-x-2 pb-2">
|
||||
@ -142,4 +179,5 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -1,11 +1,14 @@
|
||||
<script lang="ts">
|
||||
export let application: any;
|
||||
import { page } from '$app/stores';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { get } from '$lib/api';
|
||||
import { status } from '$lib/store';
|
||||
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||
|
||||
const { id } = $page.params;
|
||||
|
||||
let services: any = [];
|
||||
let selectedService: any = null;
|
||||
let usageLoading = false;
|
||||
let usage = {
|
||||
MemUsage: 0,
|
||||
@ -16,20 +19,55 @@
|
||||
|
||||
async function getUsage() {
|
||||
if (usageLoading) return;
|
||||
if (!$status.application.isRunning) return;
|
||||
usageLoading = true;
|
||||
const data = await get(`/applications/${id}/usage`);
|
||||
const data = await get(`/applications/${id}/usage/${selectedService}`);
|
||||
usage = data.usage;
|
||||
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(() => {
|
||||
clearInterval(usageInterval);
|
||||
});
|
||||
onMount(async () => {
|
||||
await getUsage();
|
||||
usageInterval = setInterval(async () => {
|
||||
await getUsage();
|
||||
}, 1000);
|
||||
const response = await get(`/applications/${id}`);
|
||||
application = response.application;
|
||||
if (response.application.dockerComposeFile) {
|
||||
services = normalizeDockerServices(
|
||||
JSON.parse(response.application.dockerComposeFile).services
|
||||
);
|
||||
} else {
|
||||
services = [
|
||||
{
|
||||
name: ''
|
||||
}
|
||||
];
|
||||
await selectService('');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -38,7 +76,29 @@
|
||||
<div class="title font-bold pb-3">Monitoring</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-auto max-w-4xl px-6 py-4">
|
||||
<div class="flex gap-2 lg:gap-8 pb-4">
|
||||
{#each services as service}
|
||||
<button
|
||||
on:click={() => selectService(service, true)}
|
||||
class:bg-primary={selectedService ===
|
||||
`${application.id}${service.name ? `-${service.name}` : ''}`}
|
||||
class:bg-coolgray-200={selectedService !==
|
||||
`${application.id}${service.name ? `-${service.name}` : ''}`}
|
||||
class="w-full rounded p-5 hover:bg-primary font-bold"
|
||||
>
|
||||
{application.id}{service.name ? `-${service.name}` : ''}</button
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
{#if selectedService}
|
||||
<div class="mx-auto max-w-4xl px-6 py-4 bg-coolgray-100 border border-coolgray-200 relative">
|
||||
{#if usageLoading}
|
||||
<button
|
||||
id="streaming"
|
||||
class="btn btn-sm bg-transparent border-none loading absolute top-0 left-0 text-xs"
|
||||
/>
|
||||
<Tooltip triggeredBy="#streaming">Streaming logs</Tooltip>
|
||||
{/if}
|
||||
<div class="text-center">
|
||||
<div class="stat w-64">
|
||||
<div class="stat-title">Used Memory / Memory Limit</div>
|
||||
@ -55,4 +115,5 @@
|
||||
<div class="stat-value text-xl">{usage?.NetIO}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -21,9 +21,11 @@
|
||||
|
||||
const { id } = $page.params;
|
||||
|
||||
let loading = false;
|
||||
let publicLoading = false;
|
||||
|
||||
let loading = {
|
||||
main: false,
|
||||
public: false
|
||||
};
|
||||
let publicUrl = '';
|
||||
let appendOnly = database.settings.appendOnly;
|
||||
|
||||
let databaseDefault: any;
|
||||
@ -47,23 +49,46 @@
|
||||
databaseDbUser = '';
|
||||
}
|
||||
}
|
||||
function generateUrl(): string {
|
||||
return `${database.type}://${
|
||||
databaseDbUser ? databaseDbUser + ':' : ''
|
||||
}${databaseDbUserPassword}@${
|
||||
$status.database.isPublic
|
||||
? database.destinationDocker.remoteEngine
|
||||
? database.destinationDocker.remoteIpAddress
|
||||
: $appSession.ipv4
|
||||
: database.id
|
||||
}:${$status.database.isPublic ? database.publicPort : privatePort}/${databaseDefault}`;
|
||||
function generateUrl() {
|
||||
const ipAddress = () => {
|
||||
if ($status.database.isPublic) {
|
||||
if (database.destinationDocker.remoteEngine) {
|
||||
return database.destinationDocker.remoteIpAddress;
|
||||
}
|
||||
if ($appSession.ipv6) {
|
||||
return $appSession.ipv6;
|
||||
}
|
||||
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) {
|
||||
if (name !== 'appendOnly') {
|
||||
if (publicLoading || !$status.database.isRunning) return;
|
||||
if (loading.public || !$status.database.isRunning) return;
|
||||
}
|
||||
publicLoading = true;
|
||||
loading.public = true;
|
||||
let data = {
|
||||
isPublic: $status.database.isPublic,
|
||||
appendOnly
|
||||
@ -87,12 +112,12 @@
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
publicLoading = false;
|
||||
loading.public = false;
|
||||
}
|
||||
}
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
loading = true;
|
||||
loading.main = true;
|
||||
await post(`/databases/${id}`, { ...database, isRunning: $status.database.isRunning });
|
||||
generateDbDetails();
|
||||
addToast({
|
||||
@ -102,7 +127,7 @@
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
loading.main = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -115,9 +140,9 @@
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-sm"
|
||||
class:loading
|
||||
class:bg-databases={!loading}
|
||||
disabled={loading}>{$t('forms.save')}</button
|
||||
class:loading={loading.main}
|
||||
class:bg-databases={!loading.main}
|
||||
disabled={loading.main}>{$t('forms.save')}</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
@ -175,7 +200,7 @@
|
||||
readonly
|
||||
disabled
|
||||
name="publicPort"
|
||||
value={publicLoading
|
||||
value={loading.public
|
||||
? 'Loading...'
|
||||
: $status.database.isPublic
|
||||
? database.publicPort
|
||||
@ -198,8 +223,8 @@
|
||||
<EdgeDB {database} />
|
||||
{/if}
|
||||
<div class="flex flex-col space-y-2 mt-5">
|
||||
<div>
|
||||
<label class="px-2" for="url"
|
||||
<div class="grid gap-2 grid-cols-2 auto-rows-max lg:px-10 px-2">
|
||||
<label for="url"
|
||||
>{$t('database.connection_string')}
|
||||
{#if !$status.database.isPublic && database.destinationDocker.remoteEngine}
|
||||
<Explainer
|
||||
@ -207,18 +232,21 @@
|
||||
/>
|
||||
{/if}</label
|
||||
>
|
||||
<button class="btn btn-sm" on:click|preventDefault={generateUrl}
|
||||
>Show Connection String</button
|
||||
>
|
||||
</div>
|
||||
<div class="lg:px-10 px-2">
|
||||
{#if publicUrl}
|
||||
<CopyPasswordField
|
||||
textarea={true}
|
||||
placeholder={$t('forms.generated_automatically_after_start')}
|
||||
isPasswordField={false}
|
||||
placeholder="Click on the button to generate URL"
|
||||
id="url"
|
||||
name="url"
|
||||
readonly
|
||||
disabled
|
||||
value={publicLoading || loading ? 'Loading...' : generateUrl()}
|
||||
value={loading.public ? 'Loading...' : publicUrl}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@ -228,7 +256,7 @@
|
||||
<div class="grid gap-2 grid-cols-2 auto-rows-max lg:px-10 px-2">
|
||||
<Setting
|
||||
id="isPublic"
|
||||
loading={publicLoading}
|
||||
loading={loading.public}
|
||||
bind:setting={$status.database.isPublic}
|
||||
on:click={() => changeSettings('isPublic')}
|
||||
title={$t('database.set_public')}
|
||||
@ -238,7 +266,7 @@
|
||||
{#if database.type === 'redis'}
|
||||
<Setting
|
||||
id="appendOnly"
|
||||
loading={publicLoading}
|
||||
loading={loading.public}
|
||||
bind:setting={appendOnly}
|
||||
on:click={() => changeSettings('appendOnly')}
|
||||
title={$t('database.change_append_only_mode')}
|
||||
|
@ -146,9 +146,29 @@
|
||||
try {
|
||||
numberOfGetStatus++;
|
||||
let isRunning = false;
|
||||
let isDegraded = false;
|
||||
if (buildPack) {
|
||||
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') {
|
||||
const response = await get(`/services/${id}/status`);
|
||||
isRunning = response.isRunning;
|
||||
@ -156,9 +176,13 @@
|
||||
const response = await get(`/databases/${id}/status`);
|
||||
isRunning = response.isRunning;
|
||||
}
|
||||
|
||||
if (isRunning) {
|
||||
status[id] = 'running';
|
||||
return 'running';
|
||||
} else if (isDegraded) {
|
||||
status[id] = 'degraded';
|
||||
return 'degraded';
|
||||
} else {
|
||||
status[id] = 'stopped';
|
||||
return 'stopped';
|
||||
@ -213,6 +237,7 @@
|
||||
(application.id && application.id.toLowerCase().includes($search.toLowerCase())) ||
|
||||
(application.name && application.name.toLowerCase().includes($search.toLowerCase())) ||
|
||||
(application.fqdn && application.fqdn.toLowerCase().includes($search.toLowerCase())) ||
|
||||
(application.dockerComposeConfiguration && application.dockerComposeConfiguration.toLowerCase().includes($search.toLowerCase())) ||
|
||||
(application.repository &&
|
||||
application.repository.toLowerCase().includes($search.toLowerCase())) ||
|
||||
(application.buildpack &&
|
||||
@ -594,6 +619,11 @@
|
||||
<span class="indicator-item badge bg-yellow-300 badge-sm" />
|
||||
{:else if status[application.id] === 'running'}
|
||||
<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}
|
||||
<span class="indicator-item badge bg-error badge-sm" />
|
||||
{/if}
|
||||
@ -613,7 +643,7 @@
|
||||
<div class="h-10 text-xs">
|
||||
{#if application?.fqdn}
|
||||
<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>
|
||||
{/if}
|
||||
{#if application.destinationDocker?.name}
|
||||
|
@ -27,10 +27,12 @@
|
||||
<script lang="ts">
|
||||
export let types: any;
|
||||
|
||||
let search = '';
|
||||
let filteredTypes = types;
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { get, post } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import ServiceIcons from '$lib/components/svg/services/ServiceIcons.svelte';
|
||||
|
||||
@ -45,10 +47,50 @@
|
||||
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>
|
||||
|
||||
<div class="flex flex-wrap justify-center">
|
||||
{#each types as type}
|
||||
<div class="container lg:mx-auto lg:p-0 px-8 pt-5">
|
||||
<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">
|
||||
<form on:submit|preventDefault={() => handleSubmit(type.name)}>
|
||||
<button type="submit" class="box-selection relative text-xl font-bold hover:bg-pink-600">
|
||||
@ -59,3 +101,4 @@
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
BIN
apps/ui/static/docker-compose.png
Normal file
BIN
apps/ui/static/docker-compose.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 207 KiB |
@ -10,7 +10,7 @@ module.exports = {
|
||||
"base-100": "#323232",
|
||||
"base-200": "#242424",
|
||||
"base-300": "#181818",
|
||||
"primary": "#6d28d9",
|
||||
"primary": "#6B16ED",
|
||||
"primary-content": "#fff",
|
||||
"secondary": "#343232",
|
||||
"accent": "#343232",
|
||||
|
@ -1,6 +1,38 @@
|
||||
version: '3.8'
|
||||
|
||||
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:
|
||||
image: coollabsio/coolify-fluent-bit:1.0.0
|
||||
command: /fluent-bit/bin/fluent-bit -c /fluent-bit/etc/fluent-bit-dev.conf
|
||||
@ -8,7 +40,10 @@ services:
|
||||
volumes:
|
||||
- ./logs:/logs
|
||||
ports:
|
||||
- "24224:24224"
|
||||
- target: 24224
|
||||
published: 24224
|
||||
protocol: tcp
|
||||
mode: host
|
||||
networks:
|
||||
- coolify-infra
|
||||
networks:
|
||||
|
@ -1,21 +1,24 @@
|
||||
{
|
||||
"name": "coolify",
|
||||
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
|
||||
"version": "3.10.14",
|
||||
"version": "3.10.15",
|
||||
"license": "Apache-2.0",
|
||||
"repository": "github:coollabsio/coolify",
|
||||
"scripts": {
|
||||
"oc": "opencollective-setup",
|
||||
"translate": "pnpm run --filter i18n-converter translate",
|
||||
"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:seed": "pnpm run --filter api db:seed",
|
||||
"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:api": "NODE_ENV=development pnpm run --filter api format",
|
||||
"lint": "run-p -l -n 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:ui": "NODE_ENV=development pnpm run --filter ui dev",
|
||||
"build": "NODE_ENV=production run-p -n build:*",
|
||||
@ -34,6 +37,7 @@
|
||||
"docker",
|
||||
"self-host",
|
||||
"iaas",
|
||||
"paas",
|
||||
"heroku",
|
||||
"netlify",
|
||||
"open-source",
|
||||
|
@ -104,6 +104,7 @@ importers:
|
||||
node-os-utils: 1.3.7
|
||||
p-all: 4.0.0
|
||||
p-throttle: 5.0.0
|
||||
prisma: 4.4.0
|
||||
public-ip: 6.0.1
|
||||
pump: 3.0.0
|
||||
ssh-config: 4.1.6
|
||||
@ -120,7 +121,6 @@ importers:
|
||||
eslint-plugin-prettier: 4.2.1_tgumt6uwl2md3n6uqnggd6wvce
|
||||
nodemon: 2.0.20
|
||||
prettier: 2.7.1
|
||||
prisma: 4.4.0
|
||||
rimraf: 3.0.2
|
||||
tsconfig-paths: 4.1.0
|
||||
typescript: 4.8.4
|
||||
@ -159,6 +159,7 @@ importers:
|
||||
flowbite: 1.5.2
|
||||
flowbite-svelte: 0.26.2
|
||||
js-cookie: 3.0.1
|
||||
js-yaml: 4.1.0
|
||||
p-limit: 4.0.0
|
||||
postcss: 8.4.16
|
||||
prettier: 2.7.1
|
||||
@ -181,6 +182,7 @@ importers:
|
||||
daisyui: 2.24.2_25hquoklqeoqwmt7fwvvcyxm5e
|
||||
dayjs: 1.11.5
|
||||
js-cookie: 3.0.1
|
||||
js-yaml: 4.1.0
|
||||
p-limit: 4.0.0
|
||||
svelte-file-dropzone: 1.0.0
|
||||
svelte-select: 4.4.7
|
||||
@ -532,6 +534,7 @@ packages:
|
||||
/@prisma/engines/4.4.0:
|
||||
resolution: {integrity: sha512-Fpykccxlt9MHrAs/QpPGpI2nOiRxuLA+LiApgA59ibbf24YICZIMWd3SI2YD+q0IAIso0jCGiHhirAIbxK3RyQ==}
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
|
||||
/@rollup/pluginutils/4.2.1:
|
||||
resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==}
|
||||
@ -5078,6 +5081,7 @@ packages:
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
'@prisma/engines': 4.4.0
|
||||
dev: false
|
||||
|
||||
/private/0.1.8:
|
||||
resolution: {integrity: sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==}
|
||||
|
Loading…
Reference in New Issue
Block a user