commit
fb955e15f4
34
.github/workflows/production-release.yml
vendored
34
.github/workflows/production-release.yml
vendored
@ -5,7 +5,7 @@ on:
|
|||||||
types: [released]
|
types: [released]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
arm64-build:
|
arm64:
|
||||||
runs-on: [self-hosted, arm64]
|
runs-on: [self-hosted, arm64]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@ -31,7 +31,7 @@ jobs:
|
|||||||
tags: coollabsio/coolify:${{steps.package-version.outputs.current-version}}-arm64
|
tags: coollabsio/coolify:${{steps.package-version.outputs.current-version}}-arm64
|
||||||
cache-from: type=registry,ref=coollabsio/coolify:buildcache-arm64
|
cache-from: type=registry,ref=coollabsio/coolify:buildcache-arm64
|
||||||
cache-to: type=registry,ref=coollabsio/coolify:buildcache-arm64,mode=max
|
cache-to: type=registry,ref=coollabsio/coolify:buildcache-arm64,mode=max
|
||||||
amd64-build:
|
amd64:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@ -57,9 +57,35 @@ jobs:
|
|||||||
tags: coollabsio/coolify:${{steps.package-version.outputs.current-version}}-amd64
|
tags: coollabsio/coolify:${{steps.package-version.outputs.current-version}}-amd64
|
||||||
cache-from: type=registry,ref=coollabsio/coolify:buildcache-amd64
|
cache-from: type=registry,ref=coollabsio/coolify:buildcache-amd64
|
||||||
cache-to: type=registry,ref=coollabsio/coolify:buildcache-amd64,mode=max
|
cache-to: type=registry,ref=coollabsio/coolify:buildcache-amd64,mode=max
|
||||||
|
aarch64:
|
||||||
|
runs-on: [self-hosted, arm64]
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Get current package version
|
||||||
|
uses: martinbeentjes/npm-get-version-action@v1.2.3
|
||||||
|
id: package-version
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/aarch64
|
||||||
|
push: true
|
||||||
|
tags: coollabsio/coolify:${{steps.package-version.outputs.current-version}}-aarch64
|
||||||
|
cache-from: type=registry,ref=coollabsio/coolify:buildcache-aarch64
|
||||||
|
cache-to: type=registry,ref=coollabsio/coolify:buildcache-aarch64,mode=max
|
||||||
merge-manifest:
|
merge-manifest:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [amd64-build, arm64-build]
|
needs: [amd64, arm64, aarch64]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
@ -77,7 +103,7 @@ jobs:
|
|||||||
id: package-version
|
id: package-version
|
||||||
- name: Create & publish manifest
|
- name: Create & publish manifest
|
||||||
run: |
|
run: |
|
||||||
docker manifest create coollabsio/coolify:${{steps.package-version.outputs.current-version}} --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-amd64 --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-arm64
|
docker manifest create coollabsio/coolify:${{steps.package-version.outputs.current-version}} --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-amd64 --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-arm64 --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-aarch64
|
||||||
docker manifest push coollabsio/coolify:${{steps.package-version.outputs.current-version}}
|
docker manifest push coollabsio/coolify:${{steps.package-version.outputs.current-version}}
|
||||||
- uses: sarisia/actions-status-discord@v1
|
- uses: sarisia/actions-status-discord@v1
|
||||||
if: always()
|
if: always()
|
||||||
|
10
.github/workflows/staging-release.yml
vendored
10
.github/workflows/staging-release.yml
vendored
@ -6,7 +6,7 @@ on:
|
|||||||
- next
|
- next
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
arm64-making-something-cool:
|
arm64:
|
||||||
runs-on: [self-hosted, arm64]
|
runs-on: [self-hosted, arm64]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@ -34,7 +34,7 @@ jobs:
|
|||||||
tags: coollabsio/coolify:next-arm64
|
tags: coollabsio/coolify:next-arm64
|
||||||
cache-from: type=registry,ref=coollabsio/coolify:buildcache-next-arm64
|
cache-from: type=registry,ref=coollabsio/coolify:buildcache-next-arm64
|
||||||
cache-to: type=registry,ref=coollabsio/coolify:buildcache-next-arm64,mode=max
|
cache-to: type=registry,ref=coollabsio/coolify:buildcache-next-arm64,mode=max
|
||||||
amd64-making-something-cool:
|
amd64:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@ -59,12 +59,12 @@ jobs:
|
|||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
push: true
|
push: true
|
||||||
tags: coollabsio/coolify:next-amd64,coollabsio/coolify:next-test
|
tags: coollabsio/coolify:next-amd64
|
||||||
cache-from: type=registry,ref=coollabsio/coolify:buildcache-next-amd64
|
cache-from: type=registry,ref=coollabsio/coolify:buildcache-next-amd64
|
||||||
cache-to: type=registry,ref=coollabsio/coolify:buildcache-next-amd64,mode=max
|
cache-to: type=registry,ref=coollabsio/coolify:buildcache-next-amd64,mode=max
|
||||||
merge-manifest-to-be-cool:
|
merge-manifest:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [arm64-making-something-cool, amd64-making-something-cool]
|
needs: [arm64, amd64]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
@ -5,19 +5,27 @@ # Contribution
|
|||||||
You can ask for guidance anytime on our Discord server in the #contribution channel.
|
You can ask for guidance anytime on our Discord server in the #contribution channel.
|
||||||
|
|
||||||
## Setup your development environment
|
## Setup your development environment
|
||||||
|
### Container based development flow (recommended and the easiest)
|
||||||
|
All you need is to intall [Docker Engine 20.11+](https://docs.docker.com/engine/install/) on your local machine and run `pnpm dev:container`. It will build the base image for Coolify and start the development server inside Docker. All required ports (3000, 3001) will be exposed to your host.
|
||||||
|
|
||||||
### Github codespaces
|
### Github codespaces
|
||||||
|
|
||||||
If you have github codespaces enabled then you can just create a codespace and run `pnpm dev` to run your the dev environment. All the required dependencies and packages has been configured for you already.
|
If you have github codespaces enabled then you can just create a codespace and run `pnpm dev` to run your the dev environment. All the required dependencies and packages has been configured for you already.
|
||||||
|
|
||||||
### Gitpod
|
### Gitpod
|
||||||
|
1. Use [container based development flow](#container-based-development-flow-easiest)
|
||||||
|
2. Or setup your workspace manually:
|
||||||
|
|
||||||
If you have a [Gitpod](https://gitpod.io), you can just create a workspace from this repository, run `pnpm install && pnpm db:push && pnpm db:seed` and then `pnpm dev`. All the required dependencies and packages has been configured for you already.
|
Create a workspace from this repository, run `pnpm install && pnpm db:push && pnpm db:seed` and then `pnpm dev`. All the required dependencies and packages has been configured for you already.
|
||||||
|
|
||||||
|
> Some packages, just `pack` are not installed in this way. You cannot test all the features. Please use the [container based development flow](#container-based-development-flow-easiest).
|
||||||
|
|
||||||
### Local Machine
|
### Local Machine
|
||||||
> At the moment, Coolify `doesn't support Windows`. You must use `Linux` or `MacOS` or consider using Gitpod or Github Codespaces.
|
> At the moment, Coolify `doesn't support Windows`. You must use `Linux` or `MacOS` or consider using Gitpod or Github Codespaces.
|
||||||
|
|
||||||
- Due to the lock file, this repository is best with [pnpm](https://pnpm.io). I recommend you try and use `pnpm` because it is cool and efficient!
|
Install all the prerequisites manually to your host system. If you would not like to install anything, I suggest to use the [container based development flow](#container-based-development-flow-easiest).
|
||||||
|
|
||||||
|
- Due to the lock file, this repository is best with [pnpm](https://pnpm.io). I recommend you try and use `pnpm` because it is cool and efficient!
|
||||||
- You need to have [Docker Engine](https://docs.docker.com/engine/install/) installed locally.
|
- You need to have [Docker Engine](https://docs.docker.com/engine/install/) installed locally.
|
||||||
- You need to have [Docker Compose Plugin](https://docs.docker.com/compose/install/compose-plugin/) installed locally.
|
- You need to have [Docker Compose Plugin](https://docs.docker.com/compose/install/compose-plugin/) installed locally.
|
||||||
- You need to have [GIT LFS Support](https://git-lfs.github.com/) installed locally.
|
- You need to have [GIT LFS Support](https://git-lfs.github.com/) installed locally.
|
||||||
|
21
Dockerfile
21
Dockerfile
@ -1,5 +1,4 @@
|
|||||||
ARG PNPM_VERSION=7.11.0
|
ARG PNPM_VERSION=7.11.0
|
||||||
ARG NPM_VERSION=8.19.1
|
|
||||||
|
|
||||||
FROM node:18-slim as build
|
FROM node:18-slim as build
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@ -17,20 +16,26 @@ WORKDIR /app
|
|||||||
ENV NODE_ENV production
|
ENV NODE_ENV production
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
|
# https://download.docker.com/linux/static/stable/
|
||||||
|
ARG DOCKER_VERSION=20.10.18
|
||||||
|
# https://github.com/docker/compose/releases
|
||||||
|
# Reverted to 2.6.1 because of this https://github.com/docker/compose/issues/9704. 2.9.0 still has a bug.
|
||||||
|
ARG DOCKER_COMPOSE_VERSION=2.6.1
|
||||||
|
# https://github.com/buildpacks/pack/releases
|
||||||
|
ARG PACK_VERSION=v0.27.0
|
||||||
|
|
||||||
RUN apt update && apt -y install --no-install-recommends ca-certificates git git-lfs openssh-client curl jq cmake sqlite3 openssl psmisc python3
|
RUN apt update && apt -y install --no-install-recommends ca-certificates git git-lfs openssh-client curl jq cmake sqlite3 openssl psmisc python3
|
||||||
RUN apt-get clean autoclean && apt-get autoremove --yes && rm -rf /var/lib/{apt,dpkg,cache,log}/
|
RUN apt-get clean autoclean && apt-get autoremove --yes && rm -rf /var/lib/{apt,dpkg,cache,log}/
|
||||||
RUN npm --no-update-notifier --no-fund --global install pnpm@${PNPM_VERSION}
|
RUN npm --no-update-notifier --no-fund --global install pnpm@${PNPM_VERSION}
|
||||||
RUN npm install -g npm@${PNPM_VERSION}
|
RUN npm install -g npm@${PNPM_VERSION}
|
||||||
|
|
||||||
RUN mkdir -p ~/.docker/cli-plugins/
|
RUN mkdir -p ~/.docker/cli-plugins/
|
||||||
# https://download.docker.com/linux/static/stable/
|
|
||||||
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-20.10.9 -o /usr/bin/docker
|
|
||||||
# https://github.com/docker/compose/releases
|
|
||||||
# Reverted to 2.6.1 because of this https://github.com/docker/compose/issues/9704. 2.9.0 still has a bug.
|
|
||||||
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-compose-linux-2.6.1 -o ~/.docker/cli-plugins/docker-compose
|
|
||||||
RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker
|
|
||||||
|
|
||||||
RUN (curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.27.0/pack-v0.27.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack)
|
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-$DOCKER_VERSION -o /usr/bin/docker
|
||||||
|
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-compose-linux-$DOCKER_COMPOSE_VERSION -o ~/.docker/cli-plugins/docker-compose
|
||||||
|
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/pack-$PACK_VERSION -o /usr/local/bin/pack
|
||||||
|
|
||||||
|
RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker /usr/local/bin/pack
|
||||||
|
|
||||||
COPY --from=build /app/apps/api/build/ .
|
COPY --from=build /app/apps/api/build/ .
|
||||||
COPY --from=build /app/others/fluentbit/ ./fluentbit
|
COPY --from=build /app/others/fluentbit/ ./fluentbit
|
||||||
|
31
Dockerfile-dev
Normal file
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?
|
dockerFileLocation String?
|
||||||
denoMainFile String?
|
denoMainFile String?
|
||||||
denoOptions String?
|
denoOptions String?
|
||||||
|
dockerComposeFile String?
|
||||||
|
dockerComposeFileLocation String?
|
||||||
|
dockerComposeConfiguration String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
destinationDockerId String?
|
destinationDockerId String?
|
||||||
|
@ -6,11 +6,14 @@ import cookie from '@fastify/cookie';
|
|||||||
import multipart from '@fastify/multipart';
|
import multipart from '@fastify/multipart';
|
||||||
import path, { join } from 'path';
|
import path, { join } from 'path';
|
||||||
import autoLoad from '@fastify/autoload';
|
import autoLoad from '@fastify/autoload';
|
||||||
import { asyncExecShell, createRemoteEngineConfiguration, getDomain, isDev, listSettings, prisma, version } from './lib/common';
|
import { asyncExecShell, cleanupDockerStorage, createRemoteEngineConfiguration, decrypt, executeDockerCmd, executeSSHCmd, generateDatabaseConfiguration, getDomain, isDev, listSettings, prisma, startTraefikProxy, startTraefikTCPProxy, version } from './lib/common';
|
||||||
import { scheduler } from './lib/scheduler';
|
import { scheduler } from './lib/scheduler';
|
||||||
import { compareVersions } from 'compare-versions';
|
import { compareVersions } from 'compare-versions';
|
||||||
import Graceful from '@ladjs/graceful'
|
import Graceful from '@ladjs/graceful'
|
||||||
|
import axios from 'axios';
|
||||||
|
import fs from 'fs/promises';
|
||||||
import { verifyRemoteDockerEngineFn } from './routes/api/v1/destinations/handlers';
|
import { verifyRemoteDockerEngineFn } from './routes/api/v1/destinations/handlers';
|
||||||
|
import { checkContainer } from './lib/docker';
|
||||||
declare module 'fastify' {
|
declare module 'fastify' {
|
||||||
interface FastifyInstance {
|
interface FastifyInstance {
|
||||||
config: {
|
config: {
|
||||||
@ -72,7 +75,6 @@ const host = '0.0.0.0';
|
|||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
schema,
|
schema,
|
||||||
dotenv: true
|
dotenv: true
|
||||||
@ -131,29 +133,26 @@ const host = '0.0.0.0';
|
|||||||
if (!scheduler.workers.has('deployApplication')) {
|
if (!scheduler.workers.has('deployApplication')) {
|
||||||
scheduler.run('deployApplication');
|
scheduler.run('deployApplication');
|
||||||
}
|
}
|
||||||
if (!scheduler.workers.has('infrastructure')) {
|
|
||||||
scheduler.run('infrastructure');
|
|
||||||
}
|
|
||||||
}, 2000)
|
}, 2000)
|
||||||
|
|
||||||
// autoUpdater
|
// autoUpdater
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:autoUpdater")
|
await autoUpdater()
|
||||||
}, 60000 * 15)
|
}, 60000 * 15)
|
||||||
|
|
||||||
// cleanupStorage
|
// cleanupStorage
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupStorage")
|
await cleanupStorage()
|
||||||
}, 60000 * 10)
|
}, 60000 * 10)
|
||||||
|
|
||||||
// checkProxies and checkFluentBit
|
// checkProxies and checkFluentBit
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:checkProxies")
|
await checkProxies();
|
||||||
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:checkFluentBit")
|
await checkFluentBit();
|
||||||
}, 10000)
|
}, 10000)
|
||||||
|
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:copySSLCertificates")
|
await copySSLCertificates();
|
||||||
}, 2000)
|
}, 2000)
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@ -165,9 +164,6 @@ const host = '0.0.0.0';
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
@ -227,3 +223,237 @@ async function configureRemoteDockers() {
|
|||||||
console.log(error)
|
console.log(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function autoUpdater() {
|
||||||
|
try {
|
||||||
|
const currentVersion = version;
|
||||||
|
const { data: versions } = await axios
|
||||||
|
.get(
|
||||||
|
`https://get.coollabs.io/versions.json`
|
||||||
|
, {
|
||||||
|
params: {
|
||||||
|
appId: process.env['COOLIFY_APP_ID'] || undefined,
|
||||||
|
version: currentVersion
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const latestVersion = versions['coolify'].main.version;
|
||||||
|
const isUpdateAvailable = compareVersions(latestVersion, currentVersion);
|
||||||
|
if (isUpdateAvailable === 1) {
|
||||||
|
const activeCount = 0
|
||||||
|
if (activeCount === 0) {
|
||||||
|
if (!isDev) {
|
||||||
|
const { isAutoUpdateEnabled } = await prisma.setting.findFirst();
|
||||||
|
if (isAutoUpdateEnabled) {
|
||||||
|
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
|
||||||
|
await asyncExecShell(`env | grep '^COOLIFY' > .env`);
|
||||||
|
await asyncExecShell(
|
||||||
|
`sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env`
|
||||||
|
);
|
||||||
|
await asyncExecShell(
|
||||||
|
`docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('Updating (not really in dev mode).');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkFluentBit() {
|
||||||
|
try {
|
||||||
|
if (!isDev) {
|
||||||
|
const engine = '/var/run/docker.sock';
|
||||||
|
const { id } = await prisma.destinationDocker.findFirst({
|
||||||
|
where: { engine, network: 'coolify' }
|
||||||
|
});
|
||||||
|
const { found } = await checkContainer({ dockerId: id, container: 'coolify-fluentbit', remove: true });
|
||||||
|
if (!found) {
|
||||||
|
await asyncExecShell(`env | grep '^COOLIFY' > .env`);
|
||||||
|
await asyncExecShell(`docker compose up -d fluent-bit`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function checkProxies() {
|
||||||
|
try {
|
||||||
|
const { default: isReachable } = await import('is-port-reachable');
|
||||||
|
let portReachable;
|
||||||
|
|
||||||
|
const { arch, ipv4, ipv6 } = await listSettings();
|
||||||
|
|
||||||
|
// Coolify Proxy local
|
||||||
|
const engine = '/var/run/docker.sock';
|
||||||
|
const localDocker = await prisma.destinationDocker.findFirst({
|
||||||
|
where: { engine, network: 'coolify', isCoolifyProxyUsed: true }
|
||||||
|
});
|
||||||
|
if (localDocker) {
|
||||||
|
portReachable = await isReachable(80, { host: ipv4 || ipv6 })
|
||||||
|
if (!portReachable) {
|
||||||
|
await startTraefikProxy(localDocker.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Coolify Proxy remote
|
||||||
|
const remoteDocker = await prisma.destinationDocker.findMany({
|
||||||
|
where: { remoteEngine: true, remoteVerified: true }
|
||||||
|
});
|
||||||
|
if (remoteDocker.length > 0) {
|
||||||
|
for (const docker of remoteDocker) {
|
||||||
|
if (docker.isCoolifyProxyUsed) {
|
||||||
|
portReachable = await isReachable(80, { host: docker.remoteIpAddress })
|
||||||
|
if (!portReachable) {
|
||||||
|
await startTraefikProxy(docker.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await createRemoteEngineConfiguration(docker.id)
|
||||||
|
} catch (error) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TCP Proxies
|
||||||
|
const databasesWithPublicPort = await prisma.database.findMany({
|
||||||
|
where: { publicPort: { not: null } },
|
||||||
|
include: { settings: true, destinationDocker: true }
|
||||||
|
});
|
||||||
|
for (const database of databasesWithPublicPort) {
|
||||||
|
const { destinationDockerId, destinationDocker, publicPort, id } = database;
|
||||||
|
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
|
||||||
|
const { privatePort } = generateDatabaseConfiguration(database, arch);
|
||||||
|
await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const wordpressWithFtp = await prisma.wordpress.findMany({
|
||||||
|
where: { ftpPublicPort: { not: null } },
|
||||||
|
include: { service: { include: { destinationDocker: true } } }
|
||||||
|
});
|
||||||
|
for (const ftp of wordpressWithFtp) {
|
||||||
|
const { service, ftpPublicPort } = ftp;
|
||||||
|
const { destinationDockerId, destinationDocker, id } = service;
|
||||||
|
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
|
||||||
|
await startTraefikTCPProxy(destinationDocker, id, ftpPublicPort, 22, 'wordpressftp');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP Proxies
|
||||||
|
const minioInstances = await prisma.minio.findMany({
|
||||||
|
where: { publicPort: { not: null } },
|
||||||
|
include: { service: { include: { destinationDocker: true } } }
|
||||||
|
});
|
||||||
|
for (const minio of minioInstances) {
|
||||||
|
const { service, publicPort } = minio;
|
||||||
|
const { destinationDockerId, destinationDocker, id } = service;
|
||||||
|
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
|
||||||
|
await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copySSLCertificates() {
|
||||||
|
try {
|
||||||
|
const pAll = await import('p-all');
|
||||||
|
const actions = []
|
||||||
|
const certificates = await prisma.certificate.findMany({ include: { team: true } })
|
||||||
|
const teamIds = certificates.map(c => c.teamId)
|
||||||
|
const destinations = await prisma.destinationDocker.findMany({ where: { isCoolifyProxyUsed: true, teams: { some: { id: { in: [...teamIds] } } } } })
|
||||||
|
for (const certificate of certificates) {
|
||||||
|
const { id, key, cert } = certificate
|
||||||
|
const decryptedKey = decrypt(key)
|
||||||
|
await fs.writeFile(`/tmp/${id}-key.pem`, decryptedKey)
|
||||||
|
await fs.writeFile(`/tmp/${id}-cert.pem`, cert)
|
||||||
|
for (const destination of destinations) {
|
||||||
|
if (destination.remoteEngine) {
|
||||||
|
if (destination.remoteVerified) {
|
||||||
|
const { id: dockerId, remoteIpAddress } = destination
|
||||||
|
actions.push(async () => copyRemoteCertificates(id, dockerId, remoteIpAddress))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
actions.push(async () => copyLocalCertificates(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await pAll.default(actions, { concurrency: 1 })
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
} finally {
|
||||||
|
await asyncExecShell(`find /tmp/ -maxdepth 1 -type f -name '*-*.pem' -delete`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyRemoteCertificates(id: string, dockerId: string, remoteIpAddress: string) {
|
||||||
|
try {
|
||||||
|
await asyncExecShell(`scp /tmp/${id}-cert.pem /tmp/${id}-key.pem ${remoteIpAddress}:/tmp/`)
|
||||||
|
await executeSSHCmd({ dockerId, command: `docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'` })
|
||||||
|
await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/` })
|
||||||
|
await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/` })
|
||||||
|
} catch (error) {
|
||||||
|
console.log({ error })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function copyLocalCertificates(id: string) {
|
||||||
|
try {
|
||||||
|
await asyncExecShell(`docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'`)
|
||||||
|
await asyncExecShell(`docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/`)
|
||||||
|
await asyncExecShell(`docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/`)
|
||||||
|
} catch (error) {
|
||||||
|
console.log({ error })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cleanupStorage() {
|
||||||
|
const destinationDockers = await prisma.destinationDocker.findMany();
|
||||||
|
let enginesDone = new Set()
|
||||||
|
for (const destination of destinationDockers) {
|
||||||
|
if (enginesDone.has(destination.engine) || enginesDone.has(destination.remoteIpAddress)) return
|
||||||
|
if (destination.engine) enginesDone.add(destination.engine)
|
||||||
|
if (destination.remoteIpAddress) enginesDone.add(destination.remoteIpAddress)
|
||||||
|
|
||||||
|
let lowDiskSpace = false;
|
||||||
|
try {
|
||||||
|
let stdout = null
|
||||||
|
if (!isDev) {
|
||||||
|
const output = await executeDockerCmd({ dockerId: destination.id, command: `CONTAINER=$(docker ps -lq | head -1) && docker exec $CONTAINER sh -c 'df -kPT /'` })
|
||||||
|
stdout = output.stdout;
|
||||||
|
} else {
|
||||||
|
const output = await asyncExecShell(
|
||||||
|
`df -kPT /`
|
||||||
|
);
|
||||||
|
stdout = output.stdout;
|
||||||
|
}
|
||||||
|
let lines = stdout.trim().split('\n');
|
||||||
|
let header = lines[0];
|
||||||
|
let regex =
|
||||||
|
/^Filesystem\s+|Type\s+|1024-blocks|\s+Used|\s+Available|\s+Capacity|\s+Mounted on\s*$/g;
|
||||||
|
const boundaries = [];
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = regex.exec(header))) {
|
||||||
|
boundaries.push(match[0].length);
|
||||||
|
}
|
||||||
|
|
||||||
|
boundaries[boundaries.length - 1] = -1;
|
||||||
|
const data = lines.slice(1).map((line) => {
|
||||||
|
const cl = boundaries.map((boundary) => {
|
||||||
|
const column = boundary > 0 ? line.slice(0, boundary) : line;
|
||||||
|
line = line.slice(boundary);
|
||||||
|
return column.trim();
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
capacity: Number.parseInt(cl[5], 10) / 100
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (data.length > 0) {
|
||||||
|
const { capacity } = data[0];
|
||||||
|
if (capacity > 0.8) {
|
||||||
|
lowDiskSpace = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) { }
|
||||||
|
await cleanupDockerStorage(destination.id, lowDiskSpace, false)
|
||||||
|
}
|
||||||
|
}
|
@ -85,6 +85,7 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
baseDirectory,
|
baseDirectory,
|
||||||
publishDirectory,
|
publishDirectory,
|
||||||
dockerFileLocation,
|
dockerFileLocation,
|
||||||
|
dockerComposeConfiguration,
|
||||||
denoMainFile
|
denoMainFile
|
||||||
} = application
|
} = application
|
||||||
const currentHash = crypto
|
const currentHash = crypto
|
||||||
@ -112,17 +113,6 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
)
|
)
|
||||||
.digest('hex');
|
.digest('hex');
|
||||||
const { debug } = settings;
|
const { debug } = settings;
|
||||||
// if (concurrency === 1) {
|
|
||||||
// await prisma.build.updateMany({
|
|
||||||
// where: {
|
|
||||||
// status: { in: ['queued', 'running'] },
|
|
||||||
// id: { not: buildId },
|
|
||||||
// applicationId,
|
|
||||||
// createdAt: { lt: new Date(new Date().getTime() - 10 * 1000) }
|
|
||||||
// },
|
|
||||||
// data: { status: 'failed' }
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
let imageId = applicationId;
|
let imageId = applicationId;
|
||||||
let domain = getDomain(fqdn);
|
let domain = getDomain(fqdn);
|
||||||
const volumes =
|
const volumes =
|
||||||
@ -138,6 +128,10 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
repository = sourceRepository || repository;
|
repository = sourceRepository || repository;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
dockerComposeConfiguration = JSON.parse(dockerComposeConfiguration)
|
||||||
|
} catch (error) { }
|
||||||
|
|
||||||
let deployNeeded = true;
|
let deployNeeded = true;
|
||||||
let destinationType;
|
let destinationType;
|
||||||
|
|
||||||
@ -212,17 +206,37 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
//
|
//
|
||||||
}
|
}
|
||||||
await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId, baseImage);
|
await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId, baseImage);
|
||||||
|
const labels = makeLabelForStandaloneApplication({
|
||||||
|
applicationId,
|
||||||
|
fqdn,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
pullmergeRequestId,
|
||||||
|
buildPack,
|
||||||
|
repository,
|
||||||
|
branch,
|
||||||
|
projectId,
|
||||||
|
port: exposePort ? `${exposePort}:${port}` : port,
|
||||||
|
commit,
|
||||||
|
installCommand,
|
||||||
|
buildCommand,
|
||||||
|
startCommand,
|
||||||
|
baseDirectory,
|
||||||
|
publishDirectory
|
||||||
|
});
|
||||||
if (forceRebuild) deployNeeded = true
|
if (forceRebuild) deployNeeded = true
|
||||||
if (!imageFound || deployNeeded) {
|
if (!imageFound || deployNeeded) {
|
||||||
// if (true) {
|
|
||||||
if (buildpacks[buildPack])
|
if (buildpacks[buildPack])
|
||||||
await buildpacks[buildPack]({
|
await buildpacks[buildPack]({
|
||||||
dockerId: destinationDocker.id,
|
dockerId: destinationDocker.id,
|
||||||
|
network: destinationDocker.network,
|
||||||
buildId,
|
buildId,
|
||||||
applicationId,
|
applicationId,
|
||||||
domain,
|
domain,
|
||||||
name,
|
name,
|
||||||
type,
|
type,
|
||||||
|
volumes,
|
||||||
|
labels,
|
||||||
pullmergeRequestId,
|
pullmergeRequestId,
|
||||||
buildPack,
|
buildPack,
|
||||||
repository,
|
repository,
|
||||||
@ -244,11 +258,12 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
pythonModule,
|
pythonModule,
|
||||||
pythonVariable,
|
pythonVariable,
|
||||||
dockerFileLocation,
|
dockerFileLocation,
|
||||||
|
dockerComposeConfiguration,
|
||||||
denoMainFile,
|
denoMainFile,
|
||||||
denoOptions,
|
denoOptions,
|
||||||
baseImage,
|
baseImage,
|
||||||
baseBuildImage,
|
baseBuildImage,
|
||||||
deploymentType
|
deploymentType,
|
||||||
});
|
});
|
||||||
else {
|
else {
|
||||||
await saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId });
|
await saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId });
|
||||||
@ -257,9 +272,53 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
} else {
|
} else {
|
||||||
await saveBuildLog({ line: 'Build image already available - no rebuild required.', buildId, applicationId });
|
await saveBuildLog({ line: 'Build image already available - no rebuild required.', buildId, applicationId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (buildPack === 'compose') {
|
||||||
try {
|
try {
|
||||||
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker stop -t 0 ${imageId}` })
|
await executeDockerCmd({
|
||||||
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker rm ${imageId}` })
|
dockerId: destinationDockerId,
|
||||||
|
command: `docker ps -a --filter 'label=coolify.applicationId=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0`
|
||||||
|
})
|
||||||
|
await executeDockerCmd({
|
||||||
|
dockerId: destinationDockerId,
|
||||||
|
command: `docker ps -a --filter 'label=coolify.applicationId=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker rm --force`
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await executeDockerCmd({ debug, buildId, applicationId, dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` })
|
||||||
|
await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
|
||||||
|
await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId });
|
||||||
|
await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } });
|
||||||
|
await prisma.application.update({
|
||||||
|
where: { id: applicationId },
|
||||||
|
data: { configHash: currentHash }
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
await saveBuildLog({ line: error, buildId, applicationId });
|
||||||
|
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } })
|
||||||
|
if (foundBuild) {
|
||||||
|
await prisma.build.update({
|
||||||
|
where: { id: buildId },
|
||||||
|
data: {
|
||||||
|
status: 'failed'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await executeDockerCmd({
|
||||||
|
dockerId: destinationDockerId,
|
||||||
|
command: `docker ps -a --filter 'label=com.docker.compose.service=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0`
|
||||||
|
})
|
||||||
|
await executeDockerCmd({
|
||||||
|
dockerId: destinationDockerId,
|
||||||
|
command: `docker ps -a --filter 'label=com.docker.compose.service=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker rm --force`
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
@ -283,24 +342,7 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
|
await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
|
||||||
const labels = makeLabelForStandaloneApplication({
|
|
||||||
applicationId,
|
|
||||||
fqdn,
|
|
||||||
name,
|
|
||||||
type,
|
|
||||||
pullmergeRequestId,
|
|
||||||
buildPack,
|
|
||||||
repository,
|
|
||||||
branch,
|
|
||||||
projectId,
|
|
||||||
port: exposePort ? `${exposePort}:${port}` : port,
|
|
||||||
commit,
|
|
||||||
installCommand,
|
|
||||||
buildCommand,
|
|
||||||
startCommand,
|
|
||||||
baseDirectory,
|
|
||||||
publishDirectory
|
|
||||||
});
|
|
||||||
let envFound = false;
|
let envFound = false;
|
||||||
try {
|
try {
|
||||||
envFound = !!(await fs.stat(`${workdir}/.env`));
|
envFound = !!(await fs.stat(`${workdir}/.env`));
|
||||||
@ -328,9 +370,6 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
depends_on: [],
|
depends_on: [],
|
||||||
expose: [port],
|
expose: [port],
|
||||||
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
|
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
|
||||||
// logging: {
|
|
||||||
// driver: 'fluentd',
|
|
||||||
// },
|
|
||||||
...defaultComposeConfiguration(destinationDocker.network),
|
...defaultComposeConfiguration(destinationDocker.network),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -365,6 +404,7 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } })
|
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } })
|
||||||
if (foundBuild) {
|
if (foundBuild) {
|
||||||
|
@ -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>@');
|
line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
|
||||||
}
|
}
|
||||||
const addTimestamp = `[${generateTimestamp()}] ${line}`;
|
const addTimestamp = `[${generateTimestamp()}] ${line}`;
|
||||||
const fluentBitUrl = isDev ? 'http://localhost:24224' : 'http://coolify-fluentbit:24224';
|
const fluentBitUrl = isDev ? process.env.COOLIFY_CONTAINER_DEV === 'true' ? 'http://coolify-fluentbit:24224' : 'http://localhost:24224' : 'http://coolify-fluentbit:24224';
|
||||||
|
|
||||||
if (isDev) {
|
if (isDev && !process.env.COOLIFY_CONTAINER_DEV) {
|
||||||
console.debug(`[${applicationId}] ${addTimestamp}`);
|
console.debug(`[${applicationId}] ${addTimestamp}`);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@ -580,7 +580,8 @@ export async function buildImage({
|
|||||||
dockerId,
|
dockerId,
|
||||||
isCache = false,
|
isCache = false,
|
||||||
debug = false,
|
debug = false,
|
||||||
dockerFileLocation = '/Dockerfile'
|
dockerFileLocation = '/Dockerfile',
|
||||||
|
commit
|
||||||
}) {
|
}) {
|
||||||
if (isCache) {
|
if (isCache) {
|
||||||
await saveBuildLog({ line: `Building cache image started.`, buildId, applicationId });
|
await saveBuildLog({ line: `Building cache image started.`, buildId, applicationId });
|
||||||
@ -596,7 +597,9 @@ export async function buildImage({
|
|||||||
}
|
}
|
||||||
const dockerFile = isCache ? `${dockerFileLocation}-cache` : `${dockerFileLocation}`
|
const dockerFile = isCache ? `${dockerFileLocation}-cache` : `${dockerFileLocation}`
|
||||||
const cache = `${applicationId}:${tag}${isCache ? '-cache' : ''}`
|
const cache = `${applicationId}:${tag}${isCache ? '-cache' : ''}`
|
||||||
await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker build --progress plain -f ${workdir}/${dockerFile} -t ${cache} ${workdir}` })
|
|
||||||
|
await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker build --progress plain -f ${workdir}/${dockerFile} -t ${cache} --build-arg SOURCE_COMMIT=${commit} ${workdir}` })
|
||||||
|
|
||||||
const { status } = await prisma.build.findUnique({ where: { id: buildId } })
|
const { status } = await prisma.build.findUnique({ where: { id: buildId } })
|
||||||
if (status === 'canceled') {
|
if (status === 'canceled') {
|
||||||
throw new Error('Deployment canceled.')
|
throw new Error('Deployment canceled.')
|
||||||
@ -634,6 +637,7 @@ export function makeLabelForStandaloneApplication({
|
|||||||
return [
|
return [
|
||||||
'coolify.managed=true',
|
'coolify.managed=true',
|
||||||
`coolify.version=${version}`,
|
`coolify.version=${version}`,
|
||||||
|
`coolify.applicationId=${applicationId}`,
|
||||||
`coolify.type=standalone-application`,
|
`coolify.type=standalone-application`,
|
||||||
`coolify.configuration=${base64Encode(
|
`coolify.configuration=${base64Encode(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
|
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(`RUN deno cache ${denoMainFile}`);
|
||||||
Dockerfile.push(`ENV NO_COLOR true`);
|
Dockerfile.push(`ENV NO_COLOR true`);
|
||||||
Dockerfile.push(`EXPOSE ${port}`);
|
Dockerfile.push(`EXPOSE ${port}`);
|
||||||
Dockerfile.push(`CMD deno run ${denoOptions ? denoOptions.split(' ') : ''} ${denoMainFile}`);
|
Dockerfile.push(`CMD deno run ${denoOptions || ''} ${denoMainFile}`);
|
||||||
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
|
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ import python from './python';
|
|||||||
import deno from './deno';
|
import deno from './deno';
|
||||||
import laravel from './laravel';
|
import laravel from './laravel';
|
||||||
import heroku from './heroku';
|
import heroku from './heroku';
|
||||||
|
import compose from './compose'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
node,
|
node,
|
||||||
@ -35,5 +36,6 @@ export {
|
|||||||
python,
|
python,
|
||||||
deno,
|
deno,
|
||||||
laravel,
|
laravel,
|
||||||
heroku
|
heroku,
|
||||||
|
compose
|
||||||
};
|
};
|
||||||
|
@ -20,7 +20,7 @@ import { scheduler } from './scheduler';
|
|||||||
import { supportedServiceTypesAndVersions } from './services/supportedVersions';
|
import { supportedServiceTypesAndVersions } from './services/supportedVersions';
|
||||||
import { includeServices } from './services/common';
|
import { includeServices } from './services/common';
|
||||||
|
|
||||||
export const version = '3.10.14';
|
export const version = '3.10.15';
|
||||||
export const isDev = process.env.NODE_ENV === 'development';
|
export const isDev = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
const algorithm = 'aes-256-ctr';
|
const algorithm = 'aes-256-ctr';
|
||||||
@ -264,7 +264,9 @@ export async function isDomainConfigured({
|
|||||||
where: {
|
where: {
|
||||||
OR: [
|
OR: [
|
||||||
{ fqdn: { endsWith: `//${nakedDomain}` } },
|
{ fqdn: { endsWith: `//${nakedDomain}` } },
|
||||||
{ fqdn: { endsWith: `//www.${nakedDomain}` } }
|
{ fqdn: { endsWith: `//www.${nakedDomain}` } },
|
||||||
|
{ dockerComposeConfiguration: { contains: `//${nakedDomain}` } },
|
||||||
|
{ dockerComposeConfiguration: { contains: `//www.${nakedDomain}` } }
|
||||||
],
|
],
|
||||||
id: { not: id },
|
id: { not: id },
|
||||||
destinationDocker: {
|
destinationDocker: {
|
||||||
@ -598,7 +600,7 @@ export async function executeDockerCmd({ debug, buildId, applicationId, dockerId
|
|||||||
command = command.replace(/docker compose/gi, 'docker-compose');
|
command = command.replace(/docker compose/gi, 'docker-compose');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (command.startsWith(`docker build --progress plain`) || command.startsWith(`pack build`)) {
|
if (command.startsWith(`docker build`) || command.startsWith(`pack build`) || command.startsWith(`docker compose build`)) {
|
||||||
return await asyncExecShellStream({ debug, buildId, applicationId, command, engine });
|
return await asyncExecShellStream({ debug, buildId, applicationId, command, engine });
|
||||||
}
|
}
|
||||||
return await execaCommand(command, { env: { DOCKER_BUILDKIT: "1", DOCKER_HOST: engine }, shell: true })
|
return await execaCommand(command, { env: { DOCKER_BUILDKIT: "1", DOCKER_HOST: engine }, shell: true })
|
||||||
|
@ -87,6 +87,9 @@ export async function removeContainer({
|
|||||||
await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` })
|
await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` })
|
||||||
await executeDockerCmd({ dockerId, command: `docker rm ${id}` })
|
await executeDockerCmd({ dockerId, command: `docker rm ${id}` })
|
||||||
}
|
}
|
||||||
|
if (JSON.parse(stdout).Status === 'exited') {
|
||||||
|
await executeDockerCmd({ dockerId, command: `docker rm ${id}` })
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
@ -73,6 +73,4 @@ export default async function ({
|
|||||||
const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`);
|
const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`);
|
||||||
|
|
||||||
return commit.replace('\n', '');
|
return commit.replace('\n', '');
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -11,15 +11,14 @@ const options: any = {
|
|||||||
defaultExtension: 'js',
|
defaultExtension: 'js',
|
||||||
logger: new Cabin(),
|
logger: new Cabin(),
|
||||||
// logger: false,
|
// logger: false,
|
||||||
workerMessageHandler: async ({ name, message }) => {
|
// workerMessageHandler: async ({ name, message }) => {
|
||||||
if (name === 'deployApplication' && message?.deploying) {
|
// if (name === 'deployApplication' && message?.deploying) {
|
||||||
if (scheduler.workers.has('autoUpdater') || scheduler.workers.has('cleanupStorage')) {
|
// if (scheduler.workers.has('autoUpdater') || scheduler.workers.has('cleanupStorage')) {
|
||||||
scheduler.workers.get('deployApplication').postMessage('cancel')
|
// scheduler.workers.get('deployApplication').postMessage('cancel')
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
jobs: [
|
jobs: [
|
||||||
{ name: 'infrastructure' },
|
|
||||||
{ name: 'deployApplication' },
|
{ name: 'deployApplication' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
@ -1410,6 +1410,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
|
|||||||
depends_on: [
|
depends_on: [
|
||||||
`${id}-mariadb`,
|
`${id}-mariadb`,
|
||||||
`${id}-redis`,
|
`${id}-redis`,
|
||||||
|
`${id}-influxdb`,
|
||||||
],
|
],
|
||||||
environment: [
|
environment: [
|
||||||
"_APP_ENV=production",
|
"_APP_ENV=production",
|
||||||
@ -1772,14 +1773,11 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
|
|||||||
],
|
],
|
||||||
...defaultComposeConfiguration(network),
|
...defaultComposeConfiguration(network),
|
||||||
},
|
},
|
||||||
|
[`${id}-usage-timeseries`]: {
|
||||||
};
|
|
||||||
dockerCompose[id].depends_on.push(`${id}-influxdb`);
|
|
||||||
dockerCompose[`${id}-usage`] = {
|
|
||||||
image: `${image}:${version}`,
|
image: `${image}:${version}`,
|
||||||
container_name: `${id}-usage`,
|
container_name: `${id}-usage`,
|
||||||
labels: makeLabelForServices('appwrite'),
|
labels: makeLabelForServices('appwrite'),
|
||||||
entrypoint: "usage",
|
entrypoint: "usage --type=timeseries",
|
||||||
depends_on: [
|
depends_on: [
|
||||||
`${id}-mariadb`,
|
`${id}-mariadb`,
|
||||||
`${id}-influxdb`,
|
`${id}-influxdb`,
|
||||||
@ -1800,16 +1798,42 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
|
|||||||
...secrets
|
...secrets
|
||||||
],
|
],
|
||||||
...defaultComposeConfiguration(network),
|
...defaultComposeConfiguration(network),
|
||||||
}
|
},
|
||||||
dockerCompose[`${id}-influxdb`] = {
|
[`${id}-usage-database`]: {
|
||||||
|
image: `${image}:${version}`,
|
||||||
|
container_name: `${id}-usage-database`,
|
||||||
|
labels: makeLabelForServices('appwrite'),
|
||||||
|
entrypoint: "usage --type=database",
|
||||||
|
depends_on: [
|
||||||
|
`${id}-mariadb`,
|
||||||
|
`${id}-influxdb`,
|
||||||
|
],
|
||||||
|
environment: [
|
||||||
|
"_APP_ENV=production",
|
||||||
|
`_APP_OPENSSL_KEY_V1=${opensslKeyV1}`,
|
||||||
|
`_APP_DB_HOST=${mariadbHost}`,
|
||||||
|
`_APP_DB_PORT=${mariadbPort}`,
|
||||||
|
`_APP_DB_SCHEMA=${mariadbDatabase}`,
|
||||||
|
`_APP_DB_USER=${mariadbUser}`,
|
||||||
|
`_APP_DB_PASS=${mariadbPassword}`,
|
||||||
|
`_APP_INFLUXDB_HOST=${id}-influxdb`,
|
||||||
|
"_APP_INFLUXDB_PORT=8086",
|
||||||
|
`_APP_REDIS_HOST=${id}-redis`,
|
||||||
|
"_APP_REDIS_PORT=6379",
|
||||||
|
`OPEN_RUNTIMES_NETWORK=${network}`,
|
||||||
|
...secrets
|
||||||
|
],
|
||||||
|
...defaultComposeConfiguration(network),
|
||||||
|
},
|
||||||
|
[`${id}-influxdb`]: {
|
||||||
image: "appwrite/influxdb:1.5.0",
|
image: "appwrite/influxdb:1.5.0",
|
||||||
container_name: `${id}-influxdb`,
|
container_name: `${id}-influxdb`,
|
||||||
volumes: [
|
volumes: [
|
||||||
`${id}-influxdb:/var/lib/influxdb:rw`
|
`${id}-influxdb:/var/lib/influxdb:rw`
|
||||||
],
|
],
|
||||||
...defaultComposeConfiguration(network),
|
...defaultComposeConfiguration(network),
|
||||||
}
|
},
|
||||||
dockerCompose[`${id}-telegraf`] = {
|
[`${id}-telegraf`]: {
|
||||||
image: "appwrite/telegraf:1.4.0",
|
image: "appwrite/telegraf:1.4.0",
|
||||||
container_name: `${id}-telegraf`,
|
container_name: `${id}-telegraf`,
|
||||||
environment: [
|
environment: [
|
||||||
@ -1819,7 +1843,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
|
|||||||
],
|
],
|
||||||
...defaultComposeConfiguration(network),
|
...defaultComposeConfiguration(network),
|
||||||
}
|
}
|
||||||
|
};
|
||||||
const composeFile: any = {
|
const composeFile: any = {
|
||||||
version: '3.8',
|
version: '3.8',
|
||||||
services: dockerCompose,
|
services: dockerCompose,
|
||||||
@ -1868,7 +1892,9 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function startServiceContainers(dockerId, composeFileDestination) {
|
async function startServiceContainers(dockerId, composeFileDestination) {
|
||||||
|
try {
|
||||||
await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} pull` })
|
await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} pull` })
|
||||||
|
} catch (error) { }
|
||||||
await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} build --no-cache` })
|
await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} build --no-cache` })
|
||||||
await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} create` })
|
await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} create` })
|
||||||
await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} start` })
|
await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} start` })
|
||||||
|
@ -29,7 +29,8 @@ export const supportedServiceTypesAndVersions = [
|
|||||||
recommendedVersion: 'stable',
|
recommendedVersion: 'stable',
|
||||||
ports: {
|
ports: {
|
||||||
main: 8000
|
main: 8000
|
||||||
}
|
},
|
||||||
|
labels: ['analytics', 'plausible', 'plausible-analytics', 'gdpr', 'no-cookie']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'nocodb',
|
name: 'nocodb',
|
||||||
@ -39,7 +40,8 @@ export const supportedServiceTypesAndVersions = [
|
|||||||
recommendedVersion: 'latest',
|
recommendedVersion: 'latest',
|
||||||
ports: {
|
ports: {
|
||||||
main: 8080
|
main: 8080
|
||||||
}
|
},
|
||||||
|
labels: ['nocodb', 'airtable', 'database']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'minio',
|
name: 'minio',
|
||||||
@ -49,7 +51,8 @@ export const supportedServiceTypesAndVersions = [
|
|||||||
recommendedVersion: 'latest',
|
recommendedVersion: 'latest',
|
||||||
ports: {
|
ports: {
|
||||||
main: 9001
|
main: 9001
|
||||||
}
|
},
|
||||||
|
labels: ['minio', 's3', 'storage']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'vscodeserver',
|
name: 'vscodeserver',
|
||||||
@ -59,7 +62,8 @@ export const supportedServiceTypesAndVersions = [
|
|||||||
recommendedVersion: 'latest',
|
recommendedVersion: 'latest',
|
||||||
ports: {
|
ports: {
|
||||||
main: 8080
|
main: 8080
|
||||||
}
|
},
|
||||||
|
labels: ['vscodeserver', 'vscode', 'code-server', 'ide']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'wordpress',
|
name: 'wordpress',
|
||||||
@ -70,7 +74,8 @@ export const supportedServiceTypesAndVersions = [
|
|||||||
recommendedVersion: 'latest',
|
recommendedVersion: 'latest',
|
||||||
ports: {
|
ports: {
|
||||||
main: 80
|
main: 80
|
||||||
}
|
},
|
||||||
|
labels: ['wordpress', 'blog', 'cms']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'vaultwarden',
|
name: 'vaultwarden',
|
||||||
@ -80,7 +85,8 @@ export const supportedServiceTypesAndVersions = [
|
|||||||
recommendedVersion: 'latest',
|
recommendedVersion: 'latest',
|
||||||
ports: {
|
ports: {
|
||||||
main: 80
|
main: 80
|
||||||
}
|
},
|
||||||
|
labels: ['vaultwarden', 'password-manager', 'passwords']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'languagetool',
|
name: 'languagetool',
|
||||||
@ -90,7 +96,8 @@ export const supportedServiceTypesAndVersions = [
|
|||||||
recommendedVersion: 'latest',
|
recommendedVersion: 'latest',
|
||||||
ports: {
|
ports: {
|
||||||
main: 8010
|
main: 8010
|
||||||
}
|
},
|
||||||
|
labels: ['languagetool', 'grammar', 'spell-checker']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'n8n',
|
name: 'n8n',
|
||||||
@ -100,7 +107,8 @@ export const supportedServiceTypesAndVersions = [
|
|||||||
recommendedVersion: 'latest',
|
recommendedVersion: 'latest',
|
||||||
ports: {
|
ports: {
|
||||||
main: 5678
|
main: 5678
|
||||||
}
|
},
|
||||||
|
labels: ['n8n', 'workflow', 'automation', 'ifttt', 'zapier', 'nodered']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'uptimekuma',
|
name: 'uptimekuma',
|
||||||
@ -110,7 +118,8 @@ export const supportedServiceTypesAndVersions = [
|
|||||||
recommendedVersion: 'latest',
|
recommendedVersion: 'latest',
|
||||||
ports: {
|
ports: {
|
||||||
main: 3001
|
main: 3001
|
||||||
}
|
},
|
||||||
|
labels: ['uptimekuma', 'uptime', 'monitoring']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ghost',
|
name: 'ghost',
|
||||||
@ -121,7 +130,8 @@ export const supportedServiceTypesAndVersions = [
|
|||||||
recommendedVersion: 'latest',
|
recommendedVersion: 'latest',
|
||||||
ports: {
|
ports: {
|
||||||
main: 2368
|
main: 2368
|
||||||
}
|
},
|
||||||
|
labels: ['ghost', 'blog', 'cms']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'meilisearch',
|
name: 'meilisearch',
|
||||||
@ -132,7 +142,8 @@ export const supportedServiceTypesAndVersions = [
|
|||||||
recommendedVersion: 'latest',
|
recommendedVersion: 'latest',
|
||||||
ports: {
|
ports: {
|
||||||
main: 7700
|
main: 7700
|
||||||
}
|
},
|
||||||
|
labels: ['meilisearch', 'search', 'search-engine']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'umami',
|
name: 'umami',
|
||||||
@ -143,7 +154,8 @@ export const supportedServiceTypesAndVersions = [
|
|||||||
recommendedVersion: 'postgresql-latest',
|
recommendedVersion: 'postgresql-latest',
|
||||||
ports: {
|
ports: {
|
||||||
main: 3000
|
main: 3000
|
||||||
}
|
},
|
||||||
|
labels: ['umami', 'analytics', 'gdpr', 'no-cookie']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'hasura',
|
name: 'hasura',
|
||||||
@ -154,7 +166,8 @@ export const supportedServiceTypesAndVersions = [
|
|||||||
recommendedVersion: 'v2.10.0',
|
recommendedVersion: 'v2.10.0',
|
||||||
ports: {
|
ports: {
|
||||||
main: 8080
|
main: 8080
|
||||||
}
|
},
|
||||||
|
labels: ['hasura', 'graphql', 'database']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'fider',
|
name: 'fider',
|
||||||
@ -165,7 +178,8 @@ export const supportedServiceTypesAndVersions = [
|
|||||||
recommendedVersion: 'stable',
|
recommendedVersion: 'stable',
|
||||||
ports: {
|
ports: {
|
||||||
main: 3000
|
main: 3000
|
||||||
}
|
},
|
||||||
|
labels: ['fider', 'feedback', 'suggestions']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'appwrite',
|
name: 'appwrite',
|
||||||
@ -176,7 +190,8 @@ export const supportedServiceTypesAndVersions = [
|
|||||||
recommendedVersion: '1.0',
|
recommendedVersion: '1.0',
|
||||||
ports: {
|
ports: {
|
||||||
main: 80
|
main: 80
|
||||||
}
|
},
|
||||||
|
labels: ['appwrite', 'database', 'storage', 'api', 'serverless']
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
// name: 'moodle',
|
// name: 'moodle',
|
||||||
@ -198,7 +213,8 @@ export const supportedServiceTypesAndVersions = [
|
|||||||
recommendedVersion: 'latest',
|
recommendedVersion: 'latest',
|
||||||
ports: {
|
ports: {
|
||||||
main: 8000
|
main: 8000
|
||||||
}
|
},
|
||||||
|
labels: ['glitchtip', 'error-reporting', 'error', 'sentry', 'bugsnag']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'searxng',
|
name: 'searxng',
|
||||||
@ -209,7 +225,8 @@ export const supportedServiceTypesAndVersions = [
|
|||||||
recommendedVersion: 'latest',
|
recommendedVersion: 'latest',
|
||||||
ports: {
|
ports: {
|
||||||
main: 8080
|
main: 8080
|
||||||
}
|
},
|
||||||
|
labels: ['searxng', 'search', 'search-engine']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'weblate',
|
name: 'weblate',
|
||||||
@ -220,7 +237,8 @@ export const supportedServiceTypesAndVersions = [
|
|||||||
recommendedVersion: 'latest',
|
recommendedVersion: 'latest',
|
||||||
ports: {
|
ports: {
|
||||||
main: 8080
|
main: 8080
|
||||||
}
|
},
|
||||||
|
labels: ['weblate', 'translation', 'localization']
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
// name: 'taiga',
|
// name: 'taiga',
|
||||||
@ -242,7 +260,8 @@ export const supportedServiceTypesAndVersions = [
|
|||||||
recommendedVersion: 'latest',
|
recommendedVersion: 'latest',
|
||||||
ports: {
|
ports: {
|
||||||
main: 3000
|
main: 3000
|
||||||
}
|
},
|
||||||
|
labels: ['grafana', 'monitoring', 'metrics', 'dashboard']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'trilium',
|
name: 'trilium',
|
||||||
@ -253,6 +272,7 @@ export const supportedServiceTypesAndVersions = [
|
|||||||
recommendedVersion: 'latest',
|
recommendedVersion: 'latest',
|
||||||
ports: {
|
ports: {
|
||||||
main: 8080
|
main: 8080
|
||||||
}
|
},
|
||||||
|
labels: ['trilium', 'notes', 'note-taking', 'wiki']
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -110,23 +110,64 @@ export async function getApplicationStatus(request: FastifyRequest<OnlyId>) {
|
|||||||
try {
|
try {
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
const { teamId } = request.user
|
const { teamId } = request.user
|
||||||
|
let payload = []
|
||||||
|
const application: any = await getApplicationFromDB(id, teamId);
|
||||||
|
if (application?.destinationDockerId) {
|
||||||
|
if (application.buildPack === 'compose') {
|
||||||
|
const { stdout: containers } = await executeDockerCmd({
|
||||||
|
dockerId: application.destinationDocker.id,
|
||||||
|
command:
|
||||||
|
`docker ps -a --filter "label=coolify.applicationId=${id}" --format '{{json .}}'`
|
||||||
|
});
|
||||||
|
const containersArray = containers.trim().split('\n');
|
||||||
|
if (containersArray.length > 0 && containersArray[0] !== '') {
|
||||||
|
for (const container of containersArray) {
|
||||||
|
let isRunning = false;
|
||||||
|
let isExited = false;
|
||||||
|
let isRestarting = false;
|
||||||
|
const containerObj = JSON.parse(container);
|
||||||
|
const status = containerObj.State
|
||||||
|
if (status === 'running') {
|
||||||
|
isRunning = true;
|
||||||
|
}
|
||||||
|
if (status === 'exited') {
|
||||||
|
isExited = true;
|
||||||
|
}
|
||||||
|
if (status === 'restarting') {
|
||||||
|
isRestarting = true;
|
||||||
|
}
|
||||||
|
payload.push({
|
||||||
|
name: containerObj.Names,
|
||||||
|
status: {
|
||||||
|
isRunning,
|
||||||
|
isExited,
|
||||||
|
isRestarting
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
let isRunning = false;
|
let isRunning = false;
|
||||||
let isExited = false;
|
let isExited = false;
|
||||||
let isRestarting = false;
|
let isRestarting = false;
|
||||||
const application: any = await getApplicationFromDB(id, teamId);
|
|
||||||
if (application?.destinationDockerId) {
|
|
||||||
const status = await checkContainer({ dockerId: application.destinationDocker.id, container: id });
|
const status = await checkContainer({ dockerId: application.destinationDocker.id, container: id });
|
||||||
if (status?.found) {
|
if (status?.found) {
|
||||||
isRunning = status.status.isRunning;
|
isRunning = status.status.isRunning;
|
||||||
isExited = status.status.isExited;
|
isExited = status.status.isExited;
|
||||||
isRestarting = status.status.isRestarting
|
isRestarting = status.status.isRestarting
|
||||||
}
|
payload.push({
|
||||||
}
|
name: id,
|
||||||
return {
|
status: {
|
||||||
isRunning,
|
isRunning,
|
||||||
isRestarting,
|
|
||||||
isExited,
|
isExited,
|
||||||
};
|
isRestarting
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return payload
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
}
|
}
|
||||||
@ -289,13 +330,15 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
|
|||||||
baseImage,
|
baseImage,
|
||||||
baseBuildImage,
|
baseBuildImage,
|
||||||
deploymentType,
|
deploymentType,
|
||||||
baseDatabaseBranch
|
baseDatabaseBranch,
|
||||||
|
dockerComposeFile,
|
||||||
|
dockerComposeFileLocation,
|
||||||
|
dockerComposeConfiguration
|
||||||
} = request.body
|
} = request.body
|
||||||
if (port) port = Number(port);
|
if (port) port = Number(port);
|
||||||
if (exposePort) {
|
if (exposePort) {
|
||||||
exposePort = Number(exposePort);
|
exposePort = Number(exposePort);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { destinationDocker: { engine, remoteEngine, remoteIpAddress }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } })
|
const { destinationDocker: { engine, remoteEngine, remoteIpAddress }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } })
|
||||||
if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress })
|
if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress })
|
||||||
if (denoOptions) denoOptions = denoOptions.trim();
|
if (denoOptions) denoOptions = denoOptions.trim();
|
||||||
@ -324,6 +367,9 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
|
|||||||
baseImage,
|
baseImage,
|
||||||
baseBuildImage,
|
baseBuildImage,
|
||||||
deploymentType,
|
deploymentType,
|
||||||
|
dockerComposeFile,
|
||||||
|
dockerComposeFileLocation,
|
||||||
|
dockerComposeConfiguration,
|
||||||
...defaultConfiguration,
|
...defaultConfiguration,
|
||||||
connectedDatabase: { update: { hostedDatabaseDBName: baseDatabaseBranch } }
|
connectedDatabase: { update: { hostedDatabaseDBName: baseDatabaseBranch } }
|
||||||
}
|
}
|
||||||
@ -342,6 +388,9 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
|
|||||||
baseImage,
|
baseImage,
|
||||||
baseBuildImage,
|
baseBuildImage,
|
||||||
deploymentType,
|
deploymentType,
|
||||||
|
dockerComposeFile,
|
||||||
|
dockerComposeFileLocation,
|
||||||
|
dockerComposeConfiguration,
|
||||||
...defaultConfiguration
|
...defaultConfiguration
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -506,6 +555,21 @@ export async function stopApplication(request: FastifyRequest<OnlyId>, reply: Fa
|
|||||||
const application: any = await getApplicationFromDB(id, teamId);
|
const application: any = await getApplicationFromDB(id, teamId);
|
||||||
if (application?.destinationDockerId) {
|
if (application?.destinationDockerId) {
|
||||||
const { id: dockerId } = application.destinationDocker;
|
const { id: dockerId } = application.destinationDocker;
|
||||||
|
if (application.buildPack === 'compose') {
|
||||||
|
const { stdout: containers } = await executeDockerCmd({
|
||||||
|
dockerId: application.destinationDocker.id,
|
||||||
|
command:
|
||||||
|
`docker ps -a --filter "label=coolify.applicationId=${id}" --format '{{json .}}'`
|
||||||
|
});
|
||||||
|
const containersArray = containers.trim().split('\n');
|
||||||
|
if (containersArray.length > 0 && containersArray[0] !== '') {
|
||||||
|
for (const container of containersArray) {
|
||||||
|
const containerObj = JSON.parse(container);
|
||||||
|
await removeContainer({ id: containerObj.ID, dockerId: application.destinationDocker.id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
const { found } = await checkContainer({ dockerId, container: id });
|
const { found } = await checkContainer({ dockerId, container: id });
|
||||||
if (found) {
|
if (found) {
|
||||||
await removeContainer({ id, dockerId: application.destinationDocker.id });
|
await removeContainer({ id, dockerId: application.destinationDocker.id });
|
||||||
@ -613,6 +677,24 @@ export async function getUsage(request) {
|
|||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getUsageByContainer(request) {
|
||||||
|
try {
|
||||||
|
const { id, containerId } = request.params
|
||||||
|
const teamId = request.user?.teamId;
|
||||||
|
let usage = {};
|
||||||
|
|
||||||
|
const application: any = await getApplicationFromDB(id, teamId);
|
||||||
|
if (application.destinationDockerId) {
|
||||||
|
[usage] = await Promise.all([getContainerUsage(application.destinationDocker.id, containerId)]);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
usage
|
||||||
|
}
|
||||||
|
} catch ({ status, message }) {
|
||||||
|
return errorHandler({ status, message })
|
||||||
|
}
|
||||||
|
}
|
||||||
export async function deployApplication(request: FastifyRequest<DeployApplication>) {
|
export async function deployApplication(request: FastifyRequest<DeployApplication>) {
|
||||||
try {
|
try {
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
@ -1159,7 +1241,7 @@ export async function getPreviews(request: FastifyRequest<OnlyId>) {
|
|||||||
|
|
||||||
export async function getApplicationLogs(request: FastifyRequest<GetApplicationLogs>) {
|
export async function getApplicationLogs(request: FastifyRequest<GetApplicationLogs>) {
|
||||||
try {
|
try {
|
||||||
const { id } = request.params;
|
const { id, containerId } = request.params;
|
||||||
let { since = 0 } = request.query
|
let { since = 0 } = request.query
|
||||||
if (since !== 0) {
|
if (since !== 0) {
|
||||||
since = day(since).unix();
|
since = day(since).unix();
|
||||||
@ -1170,10 +1252,8 @@ export async function getApplicationLogs(request: FastifyRequest<GetApplicationL
|
|||||||
});
|
});
|
||||||
if (destinationDockerId) {
|
if (destinationDockerId) {
|
||||||
try {
|
try {
|
||||||
// const found = await checkContainer({ dockerId, container: id })
|
|
||||||
// if (found) {
|
|
||||||
const { default: ansi } = await import('strip-ansi')
|
const { default: ansi } = await import('strip-ansi')
|
||||||
const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${id}` })
|
const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${containerId}` })
|
||||||
const stripLogsStdout = stdout.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
|
const stripLogsStdout = stdout.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
|
||||||
const stripLogsStderr = stderr.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
|
const stripLogsStderr = stderr.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
|
||||||
const logs = stripLogsStderr.concat(stripLogsStdout)
|
const logs = stripLogsStderr.concat(stripLogsStdout)
|
||||||
@ -1181,7 +1261,10 @@ export async function getApplicationLogs(request: FastifyRequest<GetApplicationL
|
|||||||
return { logs: sortedLogs }
|
return { logs: sortedLogs }
|
||||||
// }
|
// }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const { statusCode } = error;
|
const { statusCode, stderr } = error;
|
||||||
|
if (stderr.startsWith('Error: No such container')) {
|
||||||
|
return { logs: [], noContainer: true }
|
||||||
|
}
|
||||||
if (statusCode === 404) {
|
if (statusCode === 404) {
|
||||||
return {
|
return {
|
||||||
logs: []
|
logs: []
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { FastifyPluginAsync } from 'fastify';
|
import { FastifyPluginAsync } from 'fastify';
|
||||||
import { OnlyId } from '../../../../types';
|
import { OnlyId } from '../../../../types';
|
||||||
import { cancelDeployment, checkDNS, checkDomain, checkRepository, cleanupUnconfiguredApplications, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildPack, getBuilds, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getPreviewStatus, getSecrets, getStorages, getUsage, listApplications, loadPreviews, newApplication, restartApplication, restartPreview, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveConnectedDatabase, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication, updatePreviewSecret, updateSecret } from './handlers';
|
import { cancelDeployment, checkDNS, checkDomain, checkRepository, cleanupUnconfiguredApplications, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildPack, getBuilds, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getPreviewStatus, getSecrets, getStorages, getUsage, getUsageByContainer, listApplications, loadPreviews, newApplication, restartApplication, restartPreview, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveConnectedDatabase, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication, updatePreviewSecret, updateSecret } from './handlers';
|
||||||
|
|
||||||
import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuilds, GetImages, RestartPreviewApplication, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types';
|
import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuilds, GetImages, RestartPreviewApplication, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types';
|
||||||
|
|
||||||
@ -45,11 +45,13 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
|||||||
fastify.get<RestartPreviewApplication>('/:id/previews/:pullmergeRequestId/status', async (request) => await getPreviewStatus(request));
|
fastify.get<RestartPreviewApplication>('/:id/previews/:pullmergeRequestId/status', async (request) => await getPreviewStatus(request));
|
||||||
fastify.post<RestartPreviewApplication>('/:id/previews/:pullmergeRequestId/restart', async (request, reply) => await restartPreview(request, reply));
|
fastify.post<RestartPreviewApplication>('/:id/previews/:pullmergeRequestId/restart', async (request, reply) => await restartPreview(request, reply));
|
||||||
|
|
||||||
fastify.get<GetApplicationLogs>('/:id/logs', async (request) => await getApplicationLogs(request));
|
// fastify.get<GetApplicationLogs>('/:id/logs', async (request) => await getApplicationLogs(request));
|
||||||
|
fastify.get<GetApplicationLogs>('/:id/logs/:containerId', async (request) => await getApplicationLogs(request));
|
||||||
fastify.get<GetBuilds>('/:id/logs/build', async (request) => await getBuilds(request));
|
fastify.get<GetBuilds>('/:id/logs/build', async (request) => await getBuilds(request));
|
||||||
fastify.get<GetBuildIdLogs>('/:id/logs/build/:buildId', async (request) => await getBuildIdLogs(request));
|
fastify.get<GetBuildIdLogs>('/:id/logs/build/:buildId', async (request) => await getBuildIdLogs(request));
|
||||||
|
|
||||||
fastify.get('/:id/usage', async (request) => await getUsage(request))
|
fastify.get('/:id/usage', async (request) => await getUsage(request))
|
||||||
|
fastify.get('/:id/usage/:containerId', async (request) => await getUsageByContainer(request))
|
||||||
|
|
||||||
fastify.post<DeployApplication>('/:id/deploy', async (request) => await deployApplication(request))
|
fastify.post<DeployApplication>('/:id/deploy', async (request) => await deployApplication(request))
|
||||||
fastify.post<CancelDeployment>('/:id/cancel', async (request, reply) => await cancelDeployment(request, reply));
|
fastify.post<CancelDeployment>('/:id/cancel', async (request, reply) => await cancelDeployment(request, reply));
|
||||||
|
@ -21,7 +21,10 @@ export interface SaveApplication extends OnlyId {
|
|||||||
baseImage: string,
|
baseImage: string,
|
||||||
baseBuildImage: string,
|
baseBuildImage: string,
|
||||||
deploymentType: string,
|
deploymentType: string,
|
||||||
baseDatabaseBranch: string
|
baseDatabaseBranch: string,
|
||||||
|
dockerComposeFile: string,
|
||||||
|
dockerComposeFileLocation: string,
|
||||||
|
dockerComposeConfiguration: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export interface SaveApplicationSettings extends OnlyId {
|
export interface SaveApplicationSettings extends OnlyId {
|
||||||
@ -84,7 +87,11 @@ export interface DeleteStorage extends OnlyId {
|
|||||||
path: string,
|
path: string,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export interface GetApplicationLogs extends OnlyId {
|
export interface GetApplicationLogs {
|
||||||
|
Params: {
|
||||||
|
id: string,
|
||||||
|
containerId: string
|
||||||
|
}
|
||||||
Querystring: {
|
Querystring: {
|
||||||
since: number,
|
since: number,
|
||||||
}
|
}
|
||||||
|
@ -146,7 +146,7 @@ export async function showDashboard(request: FastifyRequest) {
|
|||||||
|
|
||||||
let foundUnconfiguredApplication = false;
|
let foundUnconfiguredApplication = false;
|
||||||
for (const application of applications) {
|
for (const application of applications) {
|
||||||
if (!application.buildPack || !application.destinationDockerId || !application.branch || (!application.settings?.isBot && !application?.fqdn)) {
|
if (!application.buildPack || !application.destinationDockerId || !application.branch || (!application.settings?.isBot && !application?.fqdn) && application.buildPack !== "compose") {
|
||||||
foundUnconfiguredApplication = true
|
foundUnconfiguredApplication = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -234,6 +234,8 @@ export async function traefikConfiguration(request, reply) {
|
|||||||
fqdn,
|
fqdn,
|
||||||
id,
|
id,
|
||||||
port,
|
port,
|
||||||
|
buildPack,
|
||||||
|
dockerComposeConfiguration,
|
||||||
destinationDocker,
|
destinationDocker,
|
||||||
destinationDockerId,
|
destinationDockerId,
|
||||||
settings: { previews, dualCerts, isCustomSSL }
|
settings: { previews, dualCerts, isCustomSSL }
|
||||||
@ -241,6 +243,33 @@ export async function traefikConfiguration(request, reply) {
|
|||||||
if (destinationDockerId) {
|
if (destinationDockerId) {
|
||||||
const { network, id: dockerId } = destinationDocker;
|
const { network, id: dockerId } = destinationDocker;
|
||||||
const isRunning = true;
|
const isRunning = true;
|
||||||
|
if (buildPack === 'compose') {
|
||||||
|
const services = Object.entries(JSON.parse(dockerComposeConfiguration))
|
||||||
|
for (const service of services) {
|
||||||
|
const [key, value] = service
|
||||||
|
const { port: customPort, fqdn } = value
|
||||||
|
if (fqdn) {
|
||||||
|
const domain = getDomain(fqdn);
|
||||||
|
const nakedDomain = domain.replace(/^www\./, '');
|
||||||
|
const isHttps = fqdn.startsWith('https://');
|
||||||
|
const isWWW = fqdn.includes('www.');
|
||||||
|
data.applications.push({
|
||||||
|
id: `${id}-${key}`,
|
||||||
|
container: `${id}-${key}`,
|
||||||
|
port: customPort ? customPort : port || 3000,
|
||||||
|
domain,
|
||||||
|
nakedDomain,
|
||||||
|
isRunning,
|
||||||
|
isHttps,
|
||||||
|
isWWW,
|
||||||
|
isDualCerts: dualCerts,
|
||||||
|
isCustomSSL
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (fqdn) {
|
if (fqdn) {
|
||||||
const domain = getDomain(fqdn);
|
const domain = getDomain(fqdn);
|
||||||
const nakedDomain = domain.replace(/^www\./, '');
|
const nakedDomain = domain.replace(/^www\./, '');
|
||||||
@ -604,13 +633,41 @@ export async function remoteTraefikConfiguration(request: FastifyRequest<OnlyId>
|
|||||||
fqdn,
|
fqdn,
|
||||||
id,
|
id,
|
||||||
port,
|
port,
|
||||||
|
buildPack,
|
||||||
|
dockerComposeConfiguration,
|
||||||
destinationDocker,
|
destinationDocker,
|
||||||
destinationDockerId,
|
destinationDockerId,
|
||||||
settings: { previews, dualCerts }
|
settings: { previews, dualCerts, isCustomSSL }
|
||||||
} = application;
|
} = application;
|
||||||
if (destinationDockerId) {
|
if (destinationDockerId) {
|
||||||
const { id: dockerId, network } = destinationDocker;
|
const { id: dockerId, network } = destinationDocker;
|
||||||
const isRunning = true;
|
const isRunning = true;
|
||||||
|
if (buildPack === 'compose') {
|
||||||
|
const services = Object.entries(JSON.parse(dockerComposeConfiguration))
|
||||||
|
for (const service of services) {
|
||||||
|
const [key, value] = service
|
||||||
|
const { port: customPort, fqdn } = value
|
||||||
|
if (fqdn) {
|
||||||
|
const domain = getDomain(fqdn);
|
||||||
|
const nakedDomain = domain.replace(/^www\./, '');
|
||||||
|
const isHttps = fqdn.startsWith('https://');
|
||||||
|
const isWWW = fqdn.includes('www.');
|
||||||
|
data.applications.push({
|
||||||
|
id: `${id}-${key}`,
|
||||||
|
container: `${id}-${key}`,
|
||||||
|
port: customPort ? customPort : port || 3000,
|
||||||
|
domain,
|
||||||
|
nakedDomain,
|
||||||
|
isRunning,
|
||||||
|
isHttps,
|
||||||
|
isWWW,
|
||||||
|
isDualCerts: dualCerts,
|
||||||
|
isCustomSSL
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (fqdn) {
|
if (fqdn) {
|
||||||
const domain = getDomain(fqdn);
|
const domain = getDomain(fqdn);
|
||||||
const nakedDomain = domain.replace(/^www\./, '');
|
const nakedDomain = domain.replace(/^www\./, '');
|
||||||
@ -626,7 +683,8 @@ export async function remoteTraefikConfiguration(request: FastifyRequest<OnlyId>
|
|||||||
isRunning,
|
isRunning,
|
||||||
isHttps,
|
isHttps,
|
||||||
isWWW,
|
isWWW,
|
||||||
isDualCerts: dualCerts
|
isDualCerts: dualCerts,
|
||||||
|
isCustomSSL
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (previews) {
|
if (previews) {
|
||||||
@ -649,7 +707,8 @@ export async function remoteTraefikConfiguration(request: FastifyRequest<OnlyId>
|
|||||||
nakedDomain,
|
nakedDomain,
|
||||||
isHttps,
|
isHttps,
|
||||||
isWWW,
|
isWWW,
|
||||||
isDualCerts: dualCerts
|
isDualCerts: dualCerts,
|
||||||
|
isCustomSSL
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,6 +48,7 @@
|
|||||||
"daisyui": "2.24.2",
|
"daisyui": "2.24.2",
|
||||||
"dayjs": "1.11.5",
|
"dayjs": "1.11.5",
|
||||||
"js-cookie": "3.0.1",
|
"js-cookie": "3.0.1",
|
||||||
|
"js-yaml": "4.1.0",
|
||||||
"p-limit": "4.0.0",
|
"p-limit": "4.0.0",
|
||||||
"svelte-file-dropzone": "^1.0.0",
|
"svelte-file-dropzone": "^1.0.0",
|
||||||
"svelte-select": "4.4.7",
|
"svelte-select": "4.4.7",
|
||||||
|
@ -110,7 +110,7 @@ async function send({
|
|||||||
if (
|
if (
|
||||||
response.status === 401 &&
|
response.status === 401 &&
|
||||||
!path.startsWith('https://api.github') &&
|
!path.startsWith('https://api.github') &&
|
||||||
!path.includes('/v4/user')
|
!path.includes('/v4/')
|
||||||
) {
|
) {
|
||||||
Cookies.remove('token');
|
Cookies.remove('token');
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={`dropdown dropdown-end ${position}`}>
|
<div class={`dropdown dropdown-end ${position}`}>
|
||||||
|
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||||
<label tabindex="0" class="btn btn-circle btn-ghost btn-xs text-sky-500">
|
<label tabindex="0" class="btn btn-circle btn-ghost btn-xs text-sky-500">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-4 h-4 stroke-current"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-4 h-4 stroke-current"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||||
</label>
|
</label>
|
||||||
|
@ -40,4 +40,6 @@
|
|||||||
<Icons.Laravel {isAbsolute} />
|
<Icons.Laravel {isAbsolute} />
|
||||||
{:else if application.buildPack?.toLowerCase() === 'heroku'}
|
{:else if application.buildPack?.toLowerCase() === 'heroku'}
|
||||||
<Icons.Heroku {isAbsolute} />
|
<Icons.Heroku {isAbsolute} />
|
||||||
|
{:else if application.buildPack?.toLowerCase() === 'compose'}
|
||||||
|
<Icons.Compose {isAbsolute} />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -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 Deno } from './Deno.svelte';
|
||||||
export { default as Laravel } from './Laravel.svelte';
|
export { default as Laravel } from './Laravel.svelte';
|
||||||
export { default as Heroku } from './Heroku.svelte';
|
export { default as Heroku } from './Heroku.svelte';
|
||||||
|
export { default as Compose } from './Compose.svelte';
|
||||||
|
@ -56,6 +56,7 @@ export const isDeploymentEnabled: Writable<boolean> = writable(false);
|
|||||||
export function checkIfDeploymentEnabledApplications(isAdmin: boolean, application: any) {
|
export function checkIfDeploymentEnabledApplications(isAdmin: boolean, application: any) {
|
||||||
return (
|
return (
|
||||||
isAdmin &&
|
isAdmin &&
|
||||||
|
(application.buildPack === 'compose') ||
|
||||||
(application.fqdn || application.settings.isBot) &&
|
(application.fqdn || application.settings.isBot) &&
|
||||||
application.gitSource &&
|
application.gitSource &&
|
||||||
application.repository &&
|
application.repository &&
|
||||||
@ -74,9 +75,8 @@ export function checkIfDeploymentEnabledServices(isAdmin: boolean, service: any)
|
|||||||
}
|
}
|
||||||
export const status: Writable<any> = writable({
|
export const status: Writable<any> = writable({
|
||||||
application: {
|
application: {
|
||||||
isRunning: false,
|
statuses: [],
|
||||||
isExited: false,
|
overallStatus: 'stopped',
|
||||||
isRestarting: false,
|
|
||||||
loading: false,
|
loading: false,
|
||||||
initialLoading: true
|
initialLoading: true
|
||||||
},
|
},
|
||||||
|
@ -29,7 +29,7 @@ export function findBuildPack(pack: string, packageManager = 'npm') {
|
|||||||
port: 80
|
port: 80
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (pack === 'docker') {
|
if (pack === 'docker' || pack === 'compose') {
|
||||||
return {
|
return {
|
||||||
...metaData,
|
...metaData,
|
||||||
installCommand: null,
|
installCommand: null,
|
||||||
@ -39,6 +39,7 @@ export function findBuildPack(pack: string, packageManager = 'npm') {
|
|||||||
port: null
|
port: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pack === 'svelte') {
|
if (pack === 'svelte') {
|
||||||
return {
|
return {
|
||||||
...metaData,
|
...metaData,
|
||||||
@ -235,6 +236,14 @@ export const buildPacks = [
|
|||||||
color: 'bg-sky-700',
|
color: 'bg-sky-700',
|
||||||
isCoolifyBuildPack: true,
|
isCoolifyBuildPack: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'compose',
|
||||||
|
type: 'base',
|
||||||
|
fancyName: 'Docker Compose',
|
||||||
|
hoverColor: 'hover:bg-sky-700',
|
||||||
|
color: 'bg-sky-700',
|
||||||
|
isCoolifyBuildPack: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'svelte',
|
name: 'svelte',
|
||||||
type: 'specific',
|
type: 'specific',
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
<div class="dropdown dropdown-bottom">
|
<div class="dropdown dropdown-bottom">
|
||||||
<slot>
|
<slot>
|
||||||
<label for="new" tabindex="0" class="btn btn-sm text-sm bg-coollabs hover:bg-coollabs-100">
|
<label for="new" tabindex="0" class="btn btn-sm text-sm bg-coollabs hover:bg-coollabs-100 w-52">
|
||||||
<svg
|
<svg
|
||||||
class="h-6 w-6"
|
class="h-6 w-6"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -37,7 +37,7 @@
|
|||||||
|
|
||||||
<ul id="new" tabindex="0" class="dropdown-content menu p-2 shadow bg-coolgray-300 rounded w-52">
|
<ul id="new" tabindex="0" class="dropdown-content menu p-2 shadow bg-coolgray-300 rounded w-52">
|
||||||
<li>
|
<li>
|
||||||
<button on:click={newApplication} class="no-underline hover:bg-applications rounded-none ">
|
<button on:click={newApplication} class="no-underline hover:bg-applications tracking-wide font-bold">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-6 w-6"
|
class="h-6 w-6"
|
||||||
@ -58,7 +58,7 @@
|
|||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button on:click={newService} class="no-underline hover:bg-services rounded-none ">
|
<button on:click={newService} class="no-underline hover:bg-services tracking-wide font-bold">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-6 w-6"
|
class="h-6 w-6"
|
||||||
@ -75,7 +75,7 @@
|
|||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button on:click={newDatabase} class="no-underline hover:bg-databases rounded-none ">
|
<button on:click={newDatabase} class="no-underline hover:bg-databases tracking-wide font-bold">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-6 w-6"
|
class="h-6 w-6"
|
||||||
@ -94,7 +94,7 @@
|
|||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/sources/new" class="no-underline hover:bg-sources rounded-none ">
|
<a href="/sources/new" class="no-underline hover:bg-sources tracking-wide font-bold">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-6 w-6"
|
class="h-6 w-6"
|
||||||
@ -116,7 +116,7 @@
|
|||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/destinations/new" class="no-underline hover:bg-destinations rounded-none ">
|
<a href="/destinations/new" class="no-underline hover:bg-destinations tracking-wide font-bold">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-6 w-6"
|
class="h-6 w-6"
|
||||||
|
@ -160,12 +160,12 @@
|
|||||||
<span>Logs</span>
|
<span>Logs</span>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
class:text-stone-600={!$status.application.isRunning}
|
class:text-stone-600={$status.application.overallStatus === 'stopped'}
|
||||||
class="rounded"
|
class="rounded"
|
||||||
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/logs`}
|
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/logs`}
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href={$status.application.isRunning ? `/applications/${$page.params.id}/logs` : ''}
|
href={$status.application.overallStatus !== 'stopped' ? `/applications/${$page.params.id}/logs` : ''}
|
||||||
class="no-underline w-full"
|
class="no-underline w-full"
|
||||||
><svg
|
><svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -218,10 +218,10 @@
|
|||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
class="rounded"
|
class="rounded"
|
||||||
class:text-stone-600={!$status.application.isRunning}
|
class:text-stone-600={$status.application.overallStatus !== 'healthy'}
|
||||||
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/usage`}
|
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/usage`}
|
||||||
>
|
>
|
||||||
<a href={$status.application.isRunning ? `/applications/${$page.params.id}/usage` : ''} class="no-underline w-full"
|
<a href={$status.application.overallStatus === 'healthy' ? `/applications/${$page.params.id}/usage` : ''} class="no-underline w-full"
|
||||||
><svg
|
><svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="w-6 h-6"
|
class="w-6 h-6"
|
||||||
|
@ -59,7 +59,6 @@
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { t } from '$lib/translations';
|
import { t } from '$lib/translations';
|
||||||
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
|
|
||||||
import {
|
import {
|
||||||
appSession,
|
appSession,
|
||||||
status,
|
status,
|
||||||
@ -140,13 +139,11 @@
|
|||||||
async function stopApplication() {
|
async function stopApplication() {
|
||||||
try {
|
try {
|
||||||
$status.application.initialLoading = true;
|
$status.application.initialLoading = true;
|
||||||
// $status.application.loading = true;
|
|
||||||
await post(`/applications/${id}/stop`, {});
|
await post(`/applications/${id}/stop`, {});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorNotification(error);
|
return errorNotification(error);
|
||||||
} finally {
|
} finally {
|
||||||
$status.application.initialLoading = false;
|
$status.application.initialLoading = false;
|
||||||
// $status.application.loading = false;
|
|
||||||
await getStatus();
|
await getStatus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -154,18 +151,48 @@
|
|||||||
if ($status.application.loading) return;
|
if ($status.application.loading) return;
|
||||||
$status.application.loading = true;
|
$status.application.loading = true;
|
||||||
const data = await get(`/applications/${id}/status`);
|
const data = await get(`/applications/${id}/status`);
|
||||||
$status.application.isRunning = data.isRunning;
|
|
||||||
$status.application.isExited = data.isExited;
|
$status.application.statuses = data;
|
||||||
$status.application.isRestarting = data.isRestarting;
|
let numberOfApplications = 0;
|
||||||
|
if (application.dockerComposeConfiguration) {
|
||||||
|
numberOfApplications =
|
||||||
|
application.buildPack === 'compose'
|
||||||
|
? Object.entries(JSON.parse(application.dockerComposeConfiguration)).length
|
||||||
|
: 1;
|
||||||
|
} else {
|
||||||
|
numberOfApplications = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($status.application.statuses.length === 0) {
|
||||||
|
$status.application.overallStatus = 'stopped';
|
||||||
|
} else {
|
||||||
|
if ($status.application.statuses.length !== numberOfApplications) {
|
||||||
|
$status.application.overallStatus = 'degraded';
|
||||||
|
} else {
|
||||||
|
for (const oneStatus of $status.application.statuses) {
|
||||||
|
if (oneStatus.status.isExited || oneStatus.status.isRestarting) {
|
||||||
|
$status.application.overallStatus = 'degraded';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (oneStatus.status.isRunning) {
|
||||||
|
$status.application.overallStatus = 'healthy';
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!oneStatus.status.isExited &&
|
||||||
|
!oneStatus.status.isRestarting &&
|
||||||
|
!oneStatus.status.isRunning
|
||||||
|
) {
|
||||||
|
$status.application.overallStatus = 'stopped';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
$status.application.loading = false;
|
$status.application.loading = false;
|
||||||
$status.application.initialLoading = false;
|
$status.application.initialLoading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
$status.application.initialLoading = true;
|
$status.application.initialLoading = true;
|
||||||
$status.application.isRunning = false;
|
|
||||||
$status.application.isExited = false;
|
|
||||||
$status.application.isRestarting = false;
|
|
||||||
$status.application.loading = false;
|
$status.application.loading = false;
|
||||||
$location = null;
|
$location = null;
|
||||||
$isDeploymentEnabled = false;
|
$isDeploymentEnabled = false;
|
||||||
@ -173,15 +200,8 @@
|
|||||||
});
|
});
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
setLocation(application, settings);
|
setLocation(application, settings);
|
||||||
$status.application.isRunning = false;
|
|
||||||
$status.application.isExited = false;
|
|
||||||
$status.application.isRestarting = false;
|
|
||||||
$status.application.loading = false;
|
$status.application.loading = false;
|
||||||
if (
|
if ($isDeploymentEnabled) {
|
||||||
application.gitSourceId &&
|
|
||||||
application.destinationDockerId &&
|
|
||||||
(application.fqdn || application.settings.isBot)
|
|
||||||
) {
|
|
||||||
await getStatus();
|
await getStatus();
|
||||||
statusInterval = setInterval(async () => {
|
statusInterval = setInterval(async () => {
|
||||||
await getStatus();
|
await getStatus();
|
||||||
@ -207,11 +227,16 @@
|
|||||||
<div class="flex justify-center items-center space-x-2">
|
<div class="flex justify-center items-center space-x-2">
|
||||||
<div>Configurations</div>
|
<div>Configurations</div>
|
||||||
<div
|
<div
|
||||||
class="badge rounded uppercase"
|
class="badge badge-lg rounded uppercase"
|
||||||
class:text-green-500={$status.application.isRunning}
|
class:text-green-500={$status.application.overallStatus === 'healthy'}
|
||||||
class:text-red-500={!$status.application.isRunning}
|
class:text-yellow-400={$status.application.overallStatus === 'degraded'}
|
||||||
|
class:text-red-500={$status.application.overallStatus === 'stopped'}
|
||||||
>
|
>
|
||||||
{$status.application.isRunning ? 'Running' : 'Stopped'}
|
{$status.application.overallStatus === 'healthy'
|
||||||
|
? 'Running'
|
||||||
|
: $status.application.overallStatus === 'degraded'
|
||||||
|
? 'Degraded'
|
||||||
|
: 'Stopped'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@ -245,16 +270,15 @@
|
|||||||
<div
|
<div
|
||||||
class="pt-4 flex flex-row items-start justify-center lg:justify-end space-x-2 order-1 lg:order-2"
|
class="pt-4 flex flex-row items-start justify-center lg:justify-end space-x-2 order-1 lg:order-2"
|
||||||
>
|
>
|
||||||
{#if $status.application.isExited || $status.application.isRestarting}
|
{#if $status.application.overallStatus === 'degraded' && application.buildPack !== 'compose'}
|
||||||
<a
|
<a
|
||||||
id="applicationerror"
|
|
||||||
href={$isDeploymentEnabled ? `/applications/${id}/logs` : null}
|
href={$isDeploymentEnabled ? `/applications/${id}/logs` : null}
|
||||||
class="icons bg-transparent text-sm text-error"
|
class="btn btn-sm text-sm gap-2"
|
||||||
sveltekit:prefetch
|
sveltekit:prefetch
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="w-6 h-6"
|
class="w-6 h-6 text-red-500"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
stroke="currentcolor"
|
stroke="currentcolor"
|
||||||
@ -269,14 +293,14 @@
|
|||||||
<line x1="12" y1="8" x2="12" y2="12" />
|
<line x1="12" y1="8" x2="12" y2="12" />
|
||||||
<line x1="12" y1="16" x2="12.01" y2="16" />
|
<line x1="12" y1="16" x2="12.01" y2="16" />
|
||||||
</svg>
|
</svg>
|
||||||
|
Application Error
|
||||||
</a>
|
</a>
|
||||||
<Tooltip triggeredBy="#applicationerror">Application exited with an error!</Tooltip>
|
|
||||||
{/if}
|
{/if}
|
||||||
{#if $status.application.initialLoading}
|
{#if $status.application.initialLoading}
|
||||||
<button class="icons animate-spin bg-transparent duration-500 ease-in-out">
|
<button class="btn btn-ghost btn-sm gap-2">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-6 w-6"
|
class="h-6 w-6 animate-spin duration-500 ease-in-out"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@ -292,14 +316,14 @@
|
|||||||
<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
|
<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
|
||||||
<line x1="11" y1="19.94" x2="11" y2="19.95" />
|
<line x1="11" y1="19.94" x2="11" y2="19.95" />
|
||||||
</svg>
|
</svg>
|
||||||
|
Loading...
|
||||||
</button>
|
</button>
|
||||||
{:else if $status.application.isRunning}
|
{:else if $status.application.overallStatus === 'healthy'}
|
||||||
<button
|
<button
|
||||||
id="stop"
|
|
||||||
on:click={stopApplication}
|
on:click={stopApplication}
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!$isDeploymentEnabled}
|
disabled={!$isDeploymentEnabled}
|
||||||
class="icons bg-transparent text-error"
|
class="btn btn-sm btn-error gap-2"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -314,16 +338,14 @@
|
|||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
<rect x="6" y="5" width="4" height="14" rx="1" />
|
<rect x="6" y="5" width="4" height="14" rx="1" />
|
||||||
<rect x="14" y="5" width="4" height="14" rx="1" />
|
<rect x="14" y="5" width="4" height="14" rx="1" />
|
||||||
</svg>
|
</svg> Stop
|
||||||
</button>
|
</button>
|
||||||
<Tooltip triggeredBy="#stop">Stop</Tooltip>
|
{#if application.buildPack !== 'compose'}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
id="restart"
|
|
||||||
on:click={restartApplication}
|
on:click={restartApplication}
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!$isDeploymentEnabled}
|
disabled={!$isDeploymentEnabled}
|
||||||
class="icons bg-transparent"
|
class="btn btn-sm gap-2"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -338,14 +360,12 @@
|
|||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" />
|
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" />
|
||||||
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" />
|
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" />
|
||||||
</svg>
|
</svg> Restart
|
||||||
</button>
|
</button>
|
||||||
<Tooltip triggeredBy="#restart">Restart (useful to change secrets)</Tooltip>
|
{/if}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
id="forceredeploy"
|
|
||||||
disabled={!$isDeploymentEnabled}
|
disabled={!$isDeploymentEnabled}
|
||||||
class="icons bg-transparent "
|
class="btn btn-sm gap-2"
|
||||||
on:click={() => handleDeploySubmit(true)}
|
on:click={() => handleDeploySubmit(true)}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@ -364,17 +384,43 @@
|
|||||||
transform="rotate(-45 12 12)"
|
transform="rotate(-45 12 12)"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
|
Force Redeploy
|
||||||
</button>
|
</button>
|
||||||
<Tooltip triggeredBy="#forceredeploy">Force Redeploy (without cache)</Tooltip>
|
|
||||||
{:else if $isDeploymentEnabled && !$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
|
{:else if $isDeploymentEnabled && !$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
|
||||||
|
{#if $status.application.overallStatus === 'degraded'}
|
||||||
<button
|
<button
|
||||||
class="icons flex items-center font-bold"
|
on:click={stopApplication}
|
||||||
|
type="submit"
|
||||||
disabled={!$isDeploymentEnabled}
|
disabled={!$isDeploymentEnabled}
|
||||||
on:click={() => handleDeploySubmit(false)}
|
class="btn btn-sm btn-error gap-2"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="w-6 h-6 mr-2 text-green-500"
|
class="w-6 h-6 "
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<rect x="6" y="5" width="4" height="14" rx="1" />
|
||||||
|
<rect x="14" y="5" width="4" height="14" rx="1" />
|
||||||
|
</svg> Stop
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
<button
|
||||||
|
class="btn btn-sm gap-2"
|
||||||
|
class:btn-primary={$status.application.overallStatus !== 'degraded'}
|
||||||
|
disabled={!$isDeploymentEnabled}
|
||||||
|
on:click={() => handleDeploySubmit(false)}
|
||||||
|
>
|
||||||
|
{#if $status.application.overallStatus !== 'degraded'}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="w-6 h-6"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@ -385,12 +431,33 @@
|
|||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
<path d="M7 4v16l13 -8z" />
|
<path d="M7 4v16l13 -8z" />
|
||||||
</svg>
|
</svg>
|
||||||
Deploy
|
{:else}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="w-6 h-6"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path
|
||||||
|
d="M16.3 5h.7a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h5l-2.82 -2.82m0 5.64l2.82 -2.82"
|
||||||
|
transform="rotate(-45 12 12)"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
{$status.application.overallStatus === 'degraded'
|
||||||
|
? $status.application.statuses.length === 1
|
||||||
|
? 'Force Redeploy'
|
||||||
|
: 'Redeploy Stack'
|
||||||
|
: 'Deploy'}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if $location && $status.application.overallStatus === 'healthy'}
|
||||||
{#if $location && $status.application.isRunning}
|
<a href={$location} target="_blank" class="btn btn-sm gap-2 text-sm bg-primary"
|
||||||
<a id="openApplication" href={$location} target="_blank" class="icons bg-transparent "
|
|
||||||
><svg
|
><svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-6 w-6"
|
class="h-6 w-6"
|
||||||
@ -405,9 +472,8 @@
|
|||||||
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
|
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
|
||||||
<line x1="10" y1="14" x2="20" y2="4" />
|
<line x1="10" y1="14" x2="20" y2="4" />
|
||||||
<polyline points="15 4 20 4 20 9" />
|
<polyline points="15 4 20 4 20 9" />
|
||||||
</svg></a
|
</svg>Open</a
|
||||||
>
|
>
|
||||||
<Tooltip triggeredBy="#openApplication">Open Application</Tooltip>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -14,6 +14,9 @@
|
|||||||
export let foundConfig: any;
|
export let foundConfig: any;
|
||||||
export let scanning: any;
|
export let scanning: any;
|
||||||
export let packageManager: any;
|
export let packageManager: any;
|
||||||
|
export let dockerComposeFile: string | null = null;
|
||||||
|
export let dockerComposeFileLocation: string | null = null;
|
||||||
|
export let dockerComposeConfiguration: any = null;
|
||||||
|
|
||||||
async function handleSubmit(name: string) {
|
async function handleSubmit(name: string) {
|
||||||
try {
|
try {
|
||||||
@ -25,10 +28,20 @@
|
|||||||
delete tempBuildPack.fancyName;
|
delete tempBuildPack.fancyName;
|
||||||
delete tempBuildPack.color;
|
delete tempBuildPack.color;
|
||||||
delete tempBuildPack.hoverColor;
|
delete tempBuildPack.hoverColor;
|
||||||
|
let composeConfiguration: any = {}
|
||||||
if (foundConfig?.buildPack !== name) {
|
if (!dockerComposeConfiguration && dockerComposeFile) {
|
||||||
await post(`/applications/${id}`, { ...tempBuildPack, buildPack: name });
|
for (const [name, _] of Object.entries(JSON.parse(dockerComposeFile).services)) {
|
||||||
|
composeConfiguration[name] = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
await post(`/applications/${id}`, {
|
||||||
|
...tempBuildPack,
|
||||||
|
buildPack: name,
|
||||||
|
dockerComposeFile,
|
||||||
|
dockerComposeFileLocation,
|
||||||
|
dockerComposeConfiguration: JSON.stringify(composeConfiguration) || JSON.stringify({})
|
||||||
|
});
|
||||||
await post(`/applications/${id}/configuration/buildpack`, { buildPack: name });
|
await post(`/applications/${id}/configuration/buildpack`, { buildPack: name });
|
||||||
return await goto(from || `/applications/${id}`);
|
return await goto(from || `/applications/${id}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -95,7 +95,7 @@
|
|||||||
if (newWindow?.closed) {
|
if (newWindow?.closed) {
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
$appSession.tokens.gitlab = localStorage.getItem('gitLabToken');
|
$appSession.tokens.gitlab = localStorage.getItem('gitLabToken');
|
||||||
localStorage.removeItem('gitLabToken');
|
// localStorage.removeItem('gitLabToken');
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
|
@ -165,7 +165,7 @@
|
|||||||
placeholder="eg: https://github.com/coollabsio/nodejs-example/tree/main"
|
placeholder="eg: https://github.com/coollabsio/nodejs-example/tree/main"
|
||||||
bind:value={publicRepositoryLink}
|
bind:value={publicRepositoryLink}
|
||||||
/>
|
/>
|
||||||
<button class="btn bg-orange-600" class:loading={loading.branches} type="submit">
|
<button class="btn bg-orange-600" type="submit">
|
||||||
Load Repository
|
Load Repository
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
const response = await get(`/applications/${params.id}/configuration/buildpack`);
|
const response = await get(`/applications/${params.id}/configuration/buildpack`);
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
application,
|
||||||
...response
|
...response
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -25,22 +26,6 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
|
||||||
|
|
||||||
import { page } from '$app/stores';
|
|
||||||
import { get } from '$lib/api';
|
|
||||||
import { appSession } from '$lib/store';
|
|
||||||
import { t } from '$lib/translations';
|
|
||||||
import { buildPacks, findBuildPack, scanningTemplates } from '$lib/templates';
|
|
||||||
import { errorNotification } from '$lib/common';
|
|
||||||
import BuildPack from './_BuildPack.svelte';
|
|
||||||
|
|
||||||
const { id } = $page.params;
|
|
||||||
|
|
||||||
let scanning = true;
|
|
||||||
let foundConfig: any = null;
|
|
||||||
let packageManager = 'npm';
|
|
||||||
|
|
||||||
export let apiUrl: any;
|
export let apiUrl: any;
|
||||||
export let projectId: any;
|
export let projectId: any;
|
||||||
export let repository: any;
|
export let repository: any;
|
||||||
@ -49,6 +34,28 @@
|
|||||||
export let application: any;
|
export let application: any;
|
||||||
export let isPublicRepository: boolean;
|
export let isPublicRepository: boolean;
|
||||||
|
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { get, getAPIUrl } from '$lib/api';
|
||||||
|
import { appSession } from '$lib/store';
|
||||||
|
import { t } from '$lib/translations';
|
||||||
|
import { buildPacks, findBuildPack, scanningTemplates } from '$lib/templates';
|
||||||
|
import { errorNotification } from '$lib/common';
|
||||||
|
import BuildPack from './_BuildPack.svelte';
|
||||||
|
import yaml from 'js-yaml';
|
||||||
|
|
||||||
|
const { id } = $page.params;
|
||||||
|
|
||||||
|
let htmlUrl = application.gitSource.htmlUrl;
|
||||||
|
|
||||||
|
let scanning: boolean = true;
|
||||||
|
let foundConfig: any = null;
|
||||||
|
let packageManager: string = 'npm';
|
||||||
|
let dockerComposeFile: string | null = application.dockerComposeFile || null;
|
||||||
|
let dockerComposeFileLocation: string | null = application.dockerComposeFileLocation || null;
|
||||||
|
let dockerComposeConfiguration: any = application.dockerComposeConfiguration || null;
|
||||||
|
|
||||||
function checkPackageJSONContents({ key, json }: { key: any; json: any }) {
|
function checkPackageJSONContents({ key, json }: { key: any; json: any }) {
|
||||||
return json?.dependencies?.hasOwnProperty(key) || json?.devDependencies?.hasOwnProperty(key);
|
return json?.dependencies?.hasOwnProperty(key) || json?.devDependencies?.hasOwnProperty(key);
|
||||||
}
|
}
|
||||||
@ -60,11 +67,45 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function scanRepository(): Promise<void> {
|
async function getGitlabToken() {
|
||||||
|
return await new Promise<void>((resolve, reject) => {
|
||||||
|
const left = screen.width / 2 - 1020 / 2;
|
||||||
|
const top = screen.height / 2 - 618 / 2;
|
||||||
|
const newWindow = open(
|
||||||
|
`${htmlUrl}/oauth/authorize?client_id=${
|
||||||
|
application.gitSource.gitlabApp.appId
|
||||||
|
}&redirect_uri=${getAPIUrl()}/webhooks/gitlab&response_type=code&scope=api+email+read_repository&state=${
|
||||||
|
$page.params.id
|
||||||
|
}`,
|
||||||
|
'GitLab',
|
||||||
|
'resizable=1, scrollbars=1, fullscreen=0, height=618, width=1020,top=' +
|
||||||
|
top +
|
||||||
|
', left=' +
|
||||||
|
left +
|
||||||
|
', toolbar=0, menubar=0, status=0'
|
||||||
|
);
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
if (newWindow?.closed) {
|
||||||
|
clearInterval(timer);
|
||||||
|
$appSession.tokens.gitlab = localStorage.getItem('gitLabToken');
|
||||||
|
localStorage.removeItem('gitLabToken');
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function scanRepository(isPublicRepository: boolean): Promise<void> {
|
||||||
try {
|
try {
|
||||||
if (type === 'gitlab') {
|
if (type === 'gitlab') {
|
||||||
const files = await get(`${apiUrl}/v4/projects/${projectId}/repository/tree`, {
|
const headers = isPublicRepository
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
Authorization: `Bearer ${$appSession.tokens.gitlab}`
|
Authorization: `Bearer ${$appSession.tokens.gitlab}`
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = isPublicRepository ? `/projects/${projectId}/repository/tree` : `/v4/projects/${projectId}/repository/tree`;
|
||||||
|
const files = await get(`${apiUrl}${url}`, {
|
||||||
|
...headers
|
||||||
});
|
});
|
||||||
const packageJson = files.find(
|
const packageJson = files.find(
|
||||||
(file: { name: string; type: string }) =>
|
(file: { name: string; type: string }) =>
|
||||||
@ -82,6 +123,14 @@
|
|||||||
(file: { name: string; type: string }) =>
|
(file: { name: string; type: string }) =>
|
||||||
file.name === 'Dockerfile' && file.type === 'blob'
|
file.name === 'Dockerfile' && file.type === 'blob'
|
||||||
);
|
);
|
||||||
|
const dockerComposeFileYml = files.find(
|
||||||
|
(file: { name: string; type: string }) =>
|
||||||
|
file.name === 'docker-compose.yml' && file.type === 'blob'
|
||||||
|
);
|
||||||
|
const dockerComposeFileYaml = files.find(
|
||||||
|
(file: { name: string; type: string }) =>
|
||||||
|
file.name === 'docker-compose.yaml' && file.type === 'blob'
|
||||||
|
);
|
||||||
const cargoToml = files.find(
|
const cargoToml = files.find(
|
||||||
(file: { name: string; type: string }) =>
|
(file: { name: string; type: string }) =>
|
||||||
file.name === 'Cargo.toml' && file.type === 'blob'
|
file.name === 'Cargo.toml' && file.type === 'blob'
|
||||||
@ -105,11 +154,24 @@
|
|||||||
const laravel = files.find(
|
const laravel = files.find(
|
||||||
(file: { name: string; type: string }) => file.name === 'artisan' && file.type === 'blob'
|
(file: { name: string; type: string }) => file.name === 'artisan' && file.type === 'blob'
|
||||||
);
|
);
|
||||||
|
|
||||||
if (yarnLock) packageManager = 'yarn';
|
if (yarnLock) packageManager = 'yarn';
|
||||||
if (pnpmLock) packageManager = 'pnpm';
|
if (pnpmLock) packageManager = 'pnpm';
|
||||||
|
|
||||||
if (dockerfile) {
|
if (dockerComposeFileYml || dockerComposeFileYaml) {
|
||||||
|
foundConfig = findBuildPack('compose', packageManager);
|
||||||
|
const id = dockerComposeFileYml.id || dockerComposeFileYaml.id;
|
||||||
|
const data = await get(`${apiUrl}/v4/projects/${projectId}/repository/blobs/${id}`, {
|
||||||
|
...headers
|
||||||
|
});
|
||||||
|
if (data?.content) {
|
||||||
|
const content = atob(data.content);
|
||||||
|
const dockerComposeJson = yaml.load(content) || null;
|
||||||
|
dockerComposeFile = JSON.stringify(dockerComposeJson);
|
||||||
|
dockerComposeFileLocation = dockerComposeFileYml
|
||||||
|
? 'docker-compose.yml'
|
||||||
|
: 'docker-compose.yaml';
|
||||||
|
}
|
||||||
|
} else if (dockerfile) {
|
||||||
foundConfig = findBuildPack('docker', packageManager);
|
foundConfig = findBuildPack('docker', packageManager);
|
||||||
} else if (packageJson && !laravel) {
|
} else if (packageJson && !laravel) {
|
||||||
const path = packageJson.path;
|
const path = packageJson.path;
|
||||||
@ -135,8 +197,13 @@
|
|||||||
foundConfig = findBuildPack('node', packageManager);
|
foundConfig = findBuildPack('node', packageManager);
|
||||||
}
|
}
|
||||||
} else if (type === 'github') {
|
} else if (type === 'github') {
|
||||||
|
const headers = isPublicRepository
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
Authorization: `token ${$appSession.tokens.github}`
|
||||||
|
};
|
||||||
const files = await get(`${apiUrl}/repos/${repository}/contents?ref=${branch}`, {
|
const files = await get(`${apiUrl}/repos/${repository}/contents?ref=${branch}`, {
|
||||||
Authorization: `Bearer ${$appSession.tokens.github}`,
|
...headers,
|
||||||
Accept: 'application/vnd.github.v2.json'
|
Accept: 'application/vnd.github.v2.json'
|
||||||
});
|
});
|
||||||
const packageJson = files.find(
|
const packageJson = files.find(
|
||||||
@ -155,6 +222,14 @@
|
|||||||
(file: { name: string; type: string }) =>
|
(file: { name: string; type: string }) =>
|
||||||
file.name === 'Dockerfile' && file.type === 'file'
|
file.name === 'Dockerfile' && file.type === 'file'
|
||||||
);
|
);
|
||||||
|
const dockerComposeFileYml = files.find(
|
||||||
|
(file: { name: string; type: string }) =>
|
||||||
|
file.name === 'docker-compose.yml' && file.type === 'file'
|
||||||
|
);
|
||||||
|
const dockerComposeFileYaml = files.find(
|
||||||
|
(file: { name: string; type: string }) =>
|
||||||
|
file.name === 'docker-compose.yaml' && file.type === 'file'
|
||||||
|
);
|
||||||
const cargoToml = files.find(
|
const cargoToml = files.find(
|
||||||
(file: { name: string; type: string }) =>
|
(file: { name: string; type: string }) =>
|
||||||
file.name === 'Cargo.toml' && file.type === 'file'
|
file.name === 'Cargo.toml' && file.type === 'file'
|
||||||
@ -182,11 +257,30 @@
|
|||||||
if (yarnLock) packageManager = 'yarn';
|
if (yarnLock) packageManager = 'yarn';
|
||||||
if (pnpmLock) packageManager = 'pnpm';
|
if (pnpmLock) packageManager = 'pnpm';
|
||||||
|
|
||||||
if (dockerfile) {
|
if (dockerComposeFileYml || dockerComposeFileYaml) {
|
||||||
|
foundConfig = findBuildPack('compose', packageManager);
|
||||||
|
const data = await get(
|
||||||
|
`${apiUrl}/repos/${repository}/contents/${
|
||||||
|
dockerComposeFileYml ? 'docker-compose.yml' : 'docker-compose.yaml'
|
||||||
|
}?ref=${branch}`,
|
||||||
|
{
|
||||||
|
...headers,
|
||||||
|
Accept: 'application/vnd.github.v2.json'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (data?.content) {
|
||||||
|
const content = atob(data.content);
|
||||||
|
const dockerComposeJson = yaml.load(content) || null;
|
||||||
|
dockerComposeFile = JSON.stringify(dockerComposeJson);
|
||||||
|
dockerComposeFileLocation = dockerComposeFileYml
|
||||||
|
? 'docker-compose.yml'
|
||||||
|
: 'docker-compose.yaml';
|
||||||
|
}
|
||||||
|
} else if (dockerfile) {
|
||||||
foundConfig = findBuildPack('docker', packageManager);
|
foundConfig = findBuildPack('docker', packageManager);
|
||||||
} else if (packageJson && !laravel) {
|
} else if (packageJson && !laravel) {
|
||||||
const data: any = await get(`${packageJson.git_url}`, {
|
const data: any = await get(`${packageJson.git_url}`, {
|
||||||
Authorization: `Bearer ${$appSession.tokens.github}`,
|
...headers,
|
||||||
Accept: 'application/vnd.github.v2.raw'
|
Accept: 'application/vnd.github.v2.raw'
|
||||||
});
|
});
|
||||||
const json = JSON.parse(data) || {};
|
const json = JSON.parse(data) || {};
|
||||||
@ -214,30 +308,39 @@
|
|||||||
error.message === '401 Unauthorized'
|
error.message === '401 Unauthorized'
|
||||||
) {
|
) {
|
||||||
if (application.gitSource.gitlabAppId) {
|
if (application.gitSource.gitlabAppId) {
|
||||||
let htmlUrl = application.gitSource.htmlUrl;
|
if (!$appSession.tokens.gitlab) {
|
||||||
const left = screen.width / 2 - 1020 / 2;
|
await getGitlabToken();
|
||||||
const top = screen.height / 2 - 618 / 2;
|
|
||||||
const newWindow = open(
|
|
||||||
`${htmlUrl}/oauth/authorize?client_id=${application.gitSource.gitlabApp.appId}&redirect_uri=${window.location.origin}/webhooks/gitlab&response_type=code&scope=api+email+read_repository&state=${$page.params.id}`,
|
|
||||||
'GitLab',
|
|
||||||
'resizable=1, scrollbars=1, fullscreen=0, height=618, width=1020,top=' +
|
|
||||||
top +
|
|
||||||
', left=' +
|
|
||||||
left +
|
|
||||||
', toolbar=0, menubar=0, status=0'
|
|
||||||
);
|
|
||||||
const timer = setInterval(() => {
|
|
||||||
if (newWindow?.closed) {
|
|
||||||
clearInterval(timer);
|
|
||||||
window.location.reload();
|
|
||||||
}
|
}
|
||||||
}, 100);
|
scanRepository(isPublicRepository);
|
||||||
|
// let htmlUrl = application.gitSource.htmlUrl;
|
||||||
|
// const left = screen.width / 2 - 1020 / 2;
|
||||||
|
// const top = screen.height / 2 - 618 / 2;
|
||||||
|
// const newWindow = open(
|
||||||
|
// `${htmlUrl}/oauth/authorize?client_id=${
|
||||||
|
// application.gitSource.gitlabApp.appId
|
||||||
|
// }&redirect_uri=${getAPIUrl()}/webhooks/gitlab&response_type=code&scope=api+email+read_repository&state=${
|
||||||
|
// $page.params.id
|
||||||
|
// }`,
|
||||||
|
// 'GitLab',
|
||||||
|
// 'resizable=1, scrollbars=1, fullscreen=0, height=618, width=1020,top=' +
|
||||||
|
// top +
|
||||||
|
// ', left=' +
|
||||||
|
// left +
|
||||||
|
// ', toolbar=0, menubar=0, status=0'
|
||||||
|
// );
|
||||||
|
// const timer = setInterval(() => {
|
||||||
|
// if (newWindow?.closed) {
|
||||||
|
// clearInterval(timer);
|
||||||
|
// $appSession.tokens.gitlab = localStorage.getItem('gitLabToken');
|
||||||
|
// // localStorage.removeItem('gitLabToken' );
|
||||||
|
|
||||||
|
// }
|
||||||
|
// }, 100);
|
||||||
}
|
}
|
||||||
}
|
} else if (error.message === 'Bad credentials') {
|
||||||
if (error.message === 'Bad credentials') {
|
|
||||||
const { token } = await get(`/applications/${id}/configuration/githubToken`);
|
const { token } = await get(`/applications/${id}/configuration/githubToken`);
|
||||||
$appSession.tokens.github = token;
|
$appSession.tokens.github = token;
|
||||||
return await scanRepository();
|
return await scanRepository(isPublicRepository);
|
||||||
}
|
}
|
||||||
return errorNotification(error);
|
return errorNotification(error);
|
||||||
} finally {
|
} finally {
|
||||||
@ -246,11 +349,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (!isPublicRepository) {
|
await scanRepository(isPublicRepository);
|
||||||
await scanRepository();
|
|
||||||
} else {
|
|
||||||
scanning = false;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -276,7 +375,15 @@
|
|||||||
<div class="flex flex-wrap justify-center">
|
<div class="flex flex-wrap justify-center">
|
||||||
{#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type === 'base') as buildPack}
|
{#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type === 'base') as buildPack}
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
|
<BuildPack
|
||||||
|
{packageManager}
|
||||||
|
{buildPack}
|
||||||
|
{scanning}
|
||||||
|
bind:foundConfig
|
||||||
|
{dockerComposeFile}
|
||||||
|
{dockerComposeFileLocation}
|
||||||
|
{dockerComposeConfiguration}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
@ -28,10 +28,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let application: any;
|
export let application: any;
|
||||||
export let settings: any;
|
export let settings: any;
|
||||||
|
|
||||||
|
import yaml from 'js-yaml';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import Select from 'svelte-select';
|
import Select from 'svelte-select';
|
||||||
import { get, post } from '$lib/api';
|
import { get, getAPIUrl, post } from '$lib/api';
|
||||||
import cuid from 'cuid';
|
import cuid from 'cuid';
|
||||||
import {
|
import {
|
||||||
addToast,
|
addToast,
|
||||||
@ -45,19 +47,27 @@
|
|||||||
import { t } from '$lib/translations';
|
import { t } from '$lib/translations';
|
||||||
import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common';
|
import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common';
|
||||||
import Setting from '$lib/components/Setting.svelte';
|
import Setting from '$lib/components/Setting.svelte';
|
||||||
import Tooltip from '$lib/components/Tooltip.svelte';
|
|
||||||
import Explainer from '$lib/components/Explainer.svelte';
|
import Explainer from '$lib/components/Explainer.svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { fade } from 'svelte/transition';
|
import Beta from '$lib/components/Beta.svelte';
|
||||||
|
|
||||||
const { id } = $page.params;
|
const { id } = $page.params;
|
||||||
|
|
||||||
$: isDisabled =
|
$: isDisabled =
|
||||||
!$appSession.isAdmin || $status.application.isRunning || $status.application.initialLoading;
|
!$appSession.isAdmin ||
|
||||||
|
$status.application.overallStatus === 'degraded' ||
|
||||||
|
$status.application.overallStatus === 'healthy' ||
|
||||||
|
$status.application.initialLoading;
|
||||||
|
|
||||||
|
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
|
||||||
|
let statues: any = {};
|
||||||
let loading = false;
|
let loading = false;
|
||||||
let fqdnEl: any = null;
|
let fqdnEl: any = null;
|
||||||
let forceSave = false;
|
let forceSave = false;
|
||||||
|
let isPublicRepository = application.settings.isPublicRepository;
|
||||||
|
let apiUrl = application.gitSource.apiUrl;
|
||||||
|
let branch = application.branch;
|
||||||
|
let repository = application.repository;
|
||||||
let debug = application.settings.debug;
|
let debug = application.settings.debug;
|
||||||
let previews = application.settings.previews;
|
let previews = application.settings.previews;
|
||||||
let dualCerts = application.settings.dualCerts;
|
let dualCerts = application.settings.dualCerts;
|
||||||
@ -65,6 +75,12 @@
|
|||||||
let autodeploy = application.settings.autodeploy;
|
let autodeploy = application.settings.autodeploy;
|
||||||
let isBot = application.settings.isBot;
|
let isBot = application.settings.isBot;
|
||||||
let isDBBranching = application.settings.isDBBranching;
|
let isDBBranching = application.settings.isDBBranching;
|
||||||
|
let htmlUrl = application.gitSource.htmlUrl;
|
||||||
|
|
||||||
|
let dockerComposeFile = JSON.parse(application.dockerComposeFile) || null;
|
||||||
|
let dockerComposeServices: any[] = [];
|
||||||
|
let dockerComposeFileLocation = application.dockerComposeFileLocation;
|
||||||
|
let dockerComposeConfiguration = JSON.parse(application.dockerComposeConfiguration) || {};
|
||||||
|
|
||||||
let baseDatabaseBranch: any = application?.connectedDatabase?.hostedDatabaseDBName || null;
|
let baseDatabaseBranch: any = application?.connectedDatabase?.hostedDatabaseDBName || null;
|
||||||
let nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
|
let nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
|
||||||
@ -86,6 +102,26 @@
|
|||||||
label: 'Uvicorn'
|
label: 'Uvicorn'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function normalizeDockerServices(services: any[]) {
|
||||||
|
const tempdockerComposeServices = [];
|
||||||
|
for (const [name, data] of Object.entries(services)) {
|
||||||
|
tempdockerComposeServices.push({
|
||||||
|
name,
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const service of tempdockerComposeServices) {
|
||||||
|
if (!dockerComposeConfiguration[service.name]) {
|
||||||
|
dockerComposeConfiguration[service.name] = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tempdockerComposeServices;
|
||||||
|
}
|
||||||
|
if (dockerComposeFile?.services) {
|
||||||
|
dockerComposeServices = normalizeDockerServices(dockerComposeFile.services);
|
||||||
|
}
|
||||||
|
|
||||||
function containerClass() {
|
function containerClass() {
|
||||||
return 'text-white bg-transparent font-thin px-0 w-full border border-dashed border-coolgray-200';
|
return 'text-white bg-transparent font-thin px-0 w-full border border-dashed border-coolgray-200';
|
||||||
}
|
}
|
||||||
@ -148,7 +184,7 @@
|
|||||||
isCustomSSL = !isCustomSSL;
|
isCustomSSL = !isCustomSSL;
|
||||||
}
|
}
|
||||||
if (name === 'isBot') {
|
if (name === 'isBot') {
|
||||||
if ($status.application.isRunning) return;
|
if ($status.application.overallStatus !== 'stopped') return;
|
||||||
isBot = !isBot;
|
isBot = !isBot;
|
||||||
application.settings.isBot = isBot;
|
application.settings.isBot = isBot;
|
||||||
application.fqdn = null;
|
application.fqdn = null;
|
||||||
@ -200,26 +236,42 @@
|
|||||||
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
|
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function handleSubmit() {
|
async function handleSubmit(toast: boolean = true) {
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
loading = true;
|
if (toast) loading = true;
|
||||||
try {
|
try {
|
||||||
nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
|
nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
|
||||||
if (application.deploymentType)
|
if (application.deploymentType)
|
||||||
application.deploymentType = application.deploymentType.toLowerCase();
|
application.deploymentType = application.deploymentType.toLowerCase();
|
||||||
!isBot &&
|
if (!isBot) {
|
||||||
(await post(`/applications/${id}/check`, {
|
await post(`/applications/${id}/check`, {
|
||||||
fqdn: application.fqdn,
|
fqdn: application.fqdn,
|
||||||
forceSave,
|
forceSave,
|
||||||
dualCerts,
|
dualCerts,
|
||||||
exposePort: application.exposePort
|
exposePort: application.exposePort
|
||||||
}));
|
});
|
||||||
await post(`/applications/${id}`, { ...application, baseDatabaseBranch });
|
for (const service of dockerComposeServices) {
|
||||||
|
if (dockerComposeConfiguration[service.name].fqdn) {
|
||||||
|
await post(`/applications/${id}/check`, {
|
||||||
|
fqdn: dockerComposeConfiguration[service.name].fqdn,
|
||||||
|
forceSave,
|
||||||
|
dualCerts,
|
||||||
|
exposePort: application.exposePort
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await post(`/applications/${id}`, {
|
||||||
|
...application,
|
||||||
|
baseDatabaseBranch,
|
||||||
|
dockerComposeConfiguration: JSON.stringify(dockerComposeConfiguration)
|
||||||
|
});
|
||||||
setLocation(application, settings);
|
setLocation(application, settings);
|
||||||
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
|
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
|
||||||
|
|
||||||
forceSave = false;
|
forceSave = false;
|
||||||
|
|
||||||
|
toast &&
|
||||||
addToast({
|
addToast({
|
||||||
message: 'Configuration saved.',
|
message: 'Configuration saved.',
|
||||||
type: 'success'
|
type: 'success'
|
||||||
@ -281,10 +333,130 @@
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async function getGitlabToken() {
|
||||||
|
return await new Promise<void>((resolve, reject) => {
|
||||||
|
const left = screen.width / 2 - 1020 / 2;
|
||||||
|
const top = screen.height / 2 - 618 / 2;
|
||||||
|
const newWindow = open(
|
||||||
|
`${htmlUrl}/oauth/authorize?client_id=${
|
||||||
|
application.gitSource.gitlabApp.appId
|
||||||
|
}&redirect_uri=${getAPIUrl()}/webhooks/gitlab&response_type=code&scope=api+email+read_repository&state=${
|
||||||
|
$page.params.id
|
||||||
|
}`,
|
||||||
|
'GitLab',
|
||||||
|
'resizable=1, scrollbars=1, fullscreen=0, height=618, width=1020,top=' +
|
||||||
|
top +
|
||||||
|
', left=' +
|
||||||
|
left +
|
||||||
|
', toolbar=0, menubar=0, status=0'
|
||||||
|
);
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
if (newWindow?.closed) {
|
||||||
|
clearInterval(timer);
|
||||||
|
$appSession.tokens.gitlab = localStorage.getItem('gitLabToken');
|
||||||
|
localStorage.removeItem('gitLabToken');
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function reloadCompose() {
|
||||||
|
try {
|
||||||
|
if (application.gitSource.type === 'github') {
|
||||||
|
const headers = isPublicRepository
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
Authorization: `token ${$appSession.tokens.github}`
|
||||||
|
};
|
||||||
|
const data = await get(
|
||||||
|
`${apiUrl}/repos/${repository}/contents/${dockerComposeFileLocation}?ref=${branch}`,
|
||||||
|
{
|
||||||
|
...headers,
|
||||||
|
Accept: 'application/vnd.github.v2.json'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (data?.content) {
|
||||||
|
const content = atob(data.content);
|
||||||
|
let dockerComposeFileContent = JSON.stringify(yaml.load(content) || null);
|
||||||
|
let dockerComposeFileContentJSON = JSON.parse(dockerComposeFileContent);
|
||||||
|
dockerComposeServices = normalizeDockerServices(dockerComposeFileContentJSON?.services);
|
||||||
|
application.dockerComposeFile = dockerComposeFileContent;
|
||||||
|
await handleSubmit(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (application.gitSource.type === 'gitlab') {
|
||||||
|
if (!$appSession.tokens.gitlab) {
|
||||||
|
await getGitlabToken();
|
||||||
|
}
|
||||||
|
const headers = isPublicRepository
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
Authorization: `Bearer ${$appSession.tokens.gitlab}`
|
||||||
|
};
|
||||||
|
const url = isPublicRepository
|
||||||
|
? ``
|
||||||
|
: `/v4/projects/${application.projectId}/repository/tree`;
|
||||||
|
const files = await get(`${apiUrl}${url}`, {
|
||||||
|
...headers
|
||||||
|
});
|
||||||
|
const dockerComposeFileYml = files.find(
|
||||||
|
(file: { name: string; type: string }) =>
|
||||||
|
file.name === dockerComposeFileLocation && file.type === 'blob'
|
||||||
|
);
|
||||||
|
const id = dockerComposeFileYml.id;
|
||||||
|
|
||||||
|
const data = await get(
|
||||||
|
`${apiUrl}/v4/projects/${application.projectId}/repository/blobs/${id}`,
|
||||||
|
{
|
||||||
|
...headers
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (data?.content) {
|
||||||
|
const content = atob(data.content);
|
||||||
|
let dockerComposeFileContent = JSON.stringify(yaml.load(content) || null);
|
||||||
|
let dockerComposeFileContentJSON = JSON.parse(dockerComposeFileContent);
|
||||||
|
dockerComposeServices = normalizeDockerServices(dockerComposeFileContentJSON?.services);
|
||||||
|
application.dockerComposeFile = dockerComposeFileContent;
|
||||||
|
await handleSubmit(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addToast({
|
||||||
|
message: 'Compose file reloaded.',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
errorNotification(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$: if ($status.application.statuses) {
|
||||||
|
for (const service of dockerComposeServices) {
|
||||||
|
getStatus(service);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getStatus(service: any) {
|
||||||
|
let foundStatus = null;
|
||||||
|
const foundService = $status.application.statuses.find(
|
||||||
|
(s: any) => s.name === `${application.id}-${service.name}`
|
||||||
|
);
|
||||||
|
if (foundService) {
|
||||||
|
const statusText = foundService?.status;
|
||||||
|
if (statusText?.isRunning) {
|
||||||
|
foundStatus = 'Running';
|
||||||
|
}
|
||||||
|
if (statusText?.isExited) {
|
||||||
|
foundStatus = 'Exited';
|
||||||
|
}
|
||||||
|
if (statusText?.isRestarting) {
|
||||||
|
foundStatus = 'Restarting';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
statues[service.name] = foundStatus || 'Stopped';
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<form on:submit|preventDefault={handleSubmit}>
|
<form on:submit|preventDefault={() => handleSubmit()}>
|
||||||
<div class="mx-auto w-full">
|
<div class="mx-auto w-full">
|
||||||
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
|
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
|
||||||
<div class="title font-bold pb-3 ">General</div>
|
<div class="title font-bold pb-3 ">General</div>
|
||||||
@ -347,7 +519,7 @@
|
|||||||
<div class="grid grid-cols-2 items-center">
|
<div class="grid grid-cols-2 items-center">
|
||||||
<label for="buildPack">{$t('application.build_pack')} </label>
|
<label for="buildPack">{$t('application.build_pack')} </label>
|
||||||
{#if isDisabled}
|
{#if isDisabled}
|
||||||
<input class="uppercase w-full" disabled={isDisabled} value={application.buildPack} />
|
<input class="capitalize w-full" disabled={isDisabled} value={application.buildPack} />
|
||||||
{:else}
|
{:else}
|
||||||
<a
|
<a
|
||||||
href={`/applications/${id}/configuration/buildpack?from=/applications/${id}`}
|
href={`/applications/${id}/configuration/buildpack?from=/applications/${id}`}
|
||||||
@ -372,6 +544,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{#if application.buildPack !== 'compose'}
|
||||||
<div class="grid grid-cols-2 items-center">
|
<div class="grid grid-cols-2 items-center">
|
||||||
<Setting
|
<Setting
|
||||||
id="isBot"
|
id="isBot"
|
||||||
@ -380,10 +553,11 @@
|
|||||||
on:click={() => changeSettings('isBot')}
|
on:click={() => changeSettings('isBot')}
|
||||||
title="Is your application a bot?"
|
title="Is your application a bot?"
|
||||||
description="You can deploy applications without domains or make them to listen on the <span class='text-settings font-bold'>Exposed Port</span>.<br></Setting><br>Useful to host <span class='text-settings font-bold'>Twitch bots, regular jobs, or anything that does not require an incoming HTTP connection.</span>"
|
description="You can deploy applications without domains or make them to listen on the <span class='text-settings font-bold'>Exposed Port</span>.<br></Setting><br>Useful to host <span class='text-settings font-bold'>Twitch bots, regular jobs, or anything that does not require an incoming HTTP connection.</span>"
|
||||||
disabled={$status.application.isRunning}
|
disabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if !isBot}
|
{/if}
|
||||||
|
{#if !isBot && application.buildPack !== 'compose'}
|
||||||
<div class="grid grid-cols-2 items-center">
|
<div class="grid grid-cols-2 items-center">
|
||||||
<label for="fqdn"
|
<label for="fqdn"
|
||||||
>{$t('application.url_fqdn')}
|
>{$t('application.url_fqdn')}
|
||||||
@ -446,15 +620,15 @@
|
|||||||
<Setting
|
<Setting
|
||||||
id="dualCerts"
|
id="dualCerts"
|
||||||
dataTooltip={$t('forms.must_be_stopped_to_modify')}
|
dataTooltip={$t('forms.must_be_stopped_to_modify')}
|
||||||
disabled={$status.application.isRunning}
|
disabled={isDisabled}
|
||||||
isCenter={false}
|
isCenter={false}
|
||||||
bind:setting={dualCerts}
|
bind:setting={dualCerts}
|
||||||
title={$t('application.ssl_www_and_non_www')}
|
title={$t('application.ssl_www_and_non_www')}
|
||||||
description="Generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-settings'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both."
|
description="Generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-settings'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both."
|
||||||
on:click={() => !$status.application.isRunning && changeSettings('dualCerts')}
|
on:click={() => !isDisabled && changeSettings('dualCerts')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if isHttps}
|
{#if isHttps && application.buildPack !== 'compose'}
|
||||||
<div class="grid grid-cols-2 items-center pb-4">
|
<div class="grid grid-cols-2 items-center pb-4">
|
||||||
<Setting
|
<Setting
|
||||||
id="isCustomSSL"
|
id="isCustomSSL"
|
||||||
@ -468,8 +642,10 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{#if application.buildPack !== 'compose'}
|
||||||
<div class="title font-bold pb-3 pt-10 border-b border-coolgray-500 mb-6">Build & Deploy</div>
|
<div class="title font-bold pb-3 pt-10 border-b border-coolgray-500 mb-6">
|
||||||
|
Build & Deploy
|
||||||
|
</div>
|
||||||
<div class="grid grid-flow-row gap-2 px-4 pr-5">
|
<div class="grid grid-flow-row gap-2 px-4 pr-5">
|
||||||
{#if application.buildCommand || application.buildPack === 'rust' || application.buildPack === 'laravel'}
|
{#if application.buildCommand || application.buildPack === 'rust' || application.buildPack === 'laravel'}
|
||||||
<div class="grid grid-cols-2 items-center">
|
<div class="grid grid-cols-2 items-center">
|
||||||
@ -486,7 +662,7 @@
|
|||||||
{isDisabled}
|
{isDisabled}
|
||||||
containerClasses={isDisabled && containerClass()}
|
containerClasses={isDisabled && containerClass()}
|
||||||
id="baseBuildImages"
|
id="baseBuildImages"
|
||||||
showIndicator={!$status.application.isRunning}
|
showIndicator={!isDisabled}
|
||||||
items={application.baseBuildImages}
|
items={application.baseBuildImages}
|
||||||
on:select={selectBaseBuildImage}
|
on:select={selectBaseBuildImage}
|
||||||
value={application.baseBuildImage}
|
value={application.baseBuildImage}
|
||||||
@ -506,7 +682,7 @@
|
|||||||
{isDisabled}
|
{isDisabled}
|
||||||
containerClasses={isDisabled && containerClass()}
|
containerClasses={isDisabled && containerClass()}
|
||||||
id="baseImages"
|
id="baseImages"
|
||||||
showIndicator={!$status.application.isRunning}
|
showIndicator={!isDisabled}
|
||||||
items={application.baseImages}
|
items={application.baseImages}
|
||||||
on:select={selectBaseImage}
|
on:select={selectBaseImage}
|
||||||
value={application.baseImage}
|
value={application.baseImage}
|
||||||
@ -528,7 +704,7 @@
|
|||||||
{isDisabled}
|
{isDisabled}
|
||||||
containerClasses={isDisabled && containerClass()}
|
containerClasses={isDisabled && containerClass()}
|
||||||
id="deploymentTypes"
|
id="deploymentTypes"
|
||||||
showIndicator={!$status.application.isRunning}
|
showIndicator={!isDisabled}
|
||||||
items={['static', 'node']}
|
items={['static', 'node']}
|
||||||
on:select={selectDeploymentType}
|
on:select={selectDeploymentType}
|
||||||
value={application.deploymentType}
|
value={application.deploymentType}
|
||||||
@ -639,7 +815,9 @@
|
|||||||
<div class="grid grid-cols-2 items-center pt-4">
|
<div class="grid grid-cols-2 items-center pt-4">
|
||||||
<label for="port"
|
<label for="port"
|
||||||
>{$t('forms.port')}
|
>{$t('forms.port')}
|
||||||
<Explainer explanation={'The port your application listens on.'} /></label
|
<Explainer
|
||||||
|
explanation={'The port your application listens inside the docker container.'}
|
||||||
|
/></label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
class="w-full"
|
class="w-full"
|
||||||
@ -660,7 +838,7 @@
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
class="w-full"
|
class="w-full"
|
||||||
readonly={!$appSession.isAdmin && !$status.application.isRunning}
|
readonly={!isDisabled}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
name="exposePort"
|
name="exposePort"
|
||||||
id="exposePort"
|
id="exposePort"
|
||||||
@ -811,6 +989,98 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="title font-bold pb-3 pt-10 border-b border-coolgray-500 mb-6">
|
||||||
|
Stack <Beta />
|
||||||
|
{#if $appSession.isAdmin}
|
||||||
|
<button class="btn btn-sm btn-primary" on:click|preventDefault={reloadCompose}
|
||||||
|
>Reload Docker Compose File</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-flow-row gap-2">
|
||||||
|
{#each dockerComposeServices as service}
|
||||||
|
<div
|
||||||
|
class="grid items-center bg-coolgray-100 rounded border border-coolgray-300 p-2 px-4"
|
||||||
|
>
|
||||||
|
<div class="text-xl font-bold uppercase">
|
||||||
|
{service.name}
|
||||||
|
<span
|
||||||
|
class="badge rounded text-white"
|
||||||
|
class:text-red-500={statues[service.name] === 'Exited' ||
|
||||||
|
statues[service.name] === 'Stopped'}
|
||||||
|
class:text-yellow-400={statues[service.name] === 'Restarting'}
|
||||||
|
class:text-green-500={statues[service.name] === 'Running'}
|
||||||
|
>{statues[service.name] || 'Loading...'}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs">{application.id}-{service.name}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 items-center px-8">
|
||||||
|
<label for="fqdn"
|
||||||
|
>{$t('application.url_fqdn')}
|
||||||
|
<Explainer
|
||||||
|
explanation={"If you specify <span class='text-settings font-bold'>https</span>, the application will be accessible only over https.<br>SSL certificate will be generated automatically.<br><br>If you specify <span class='text-settings font-bold'>www</span>, the application will be redirected (302) from non-www and vice versa.<br><br>To modify the domain, you must first stop the application.<br><br><span class='text-settings font-bold'>You must set your DNS to point to the server IP in advance.</span>"}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
class="w-full"
|
||||||
|
disabled={isDisabled}
|
||||||
|
readonly={!$appSession.isAdmin}
|
||||||
|
name="fqdn"
|
||||||
|
id="fqdn"
|
||||||
|
bind:value={dockerComposeConfiguration[service.name].fqdn}
|
||||||
|
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
|
||||||
|
placeholder="eg: https://coollabs.io"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 items-center px-8">
|
||||||
|
<label for="destinationdns"
|
||||||
|
>Internal DNS on the deployed Destination
|
||||||
|
<Explainer
|
||||||
|
explanation={'You can use these DNS names to access the application from other resources in your Destination.'}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
for="destinationdns"
|
||||||
|
class="w-full"
|
||||||
|
disabled
|
||||||
|
readonly
|
||||||
|
value={`${application.id}-${service.name}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 items-center px-8">
|
||||||
|
<label for="stackdns"
|
||||||
|
>Internal DNS in the current stack
|
||||||
|
<Explainer
|
||||||
|
explanation={'You can use these DNS names to access the application from this stack.'}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<input for="stackdns" class="w-full" disabled readonly value={service.name} />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 items-center px-8 pb-4">
|
||||||
|
<label for="port"
|
||||||
|
>{$t('forms.port')}
|
||||||
|
<Explainer
|
||||||
|
explanation={'The port your application listens inside the docker container.'}
|
||||||
|
/></label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="w-full"
|
||||||
|
disabled={isDisabled}
|
||||||
|
readonly={!$appSession.isAdmin}
|
||||||
|
name="port"
|
||||||
|
id="port"
|
||||||
|
required={!!dockerComposeConfiguration[service.name].fqdn}
|
||||||
|
bind:value={dockerComposeConfiguration[service.name].port}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,11 +3,9 @@
|
|||||||
import { get } from '$lib/api';
|
import { get } from '$lib/api';
|
||||||
import { t } from '$lib/translations';
|
import { t } from '$lib/translations';
|
||||||
import { errorNotification } from '$lib/common';
|
import { errorNotification } from '$lib/common';
|
||||||
import LoadingLogs from '$lib/components/LoadingLogs.svelte';
|
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import Tooltip from '$lib/components/Tooltip.svelte';
|
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||||
import { status } from '$lib/store';
|
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
let application: any = {};
|
let application: any = {};
|
||||||
let logsLoading = false;
|
let logsLoading = false;
|
||||||
let loadLogsInterval: any = null;
|
let loadLogsInterval: any = null;
|
||||||
@ -17,47 +15,52 @@
|
|||||||
let followingLogs: any;
|
let followingLogs: any;
|
||||||
let logsEl: any;
|
let logsEl: any;
|
||||||
let position = 0;
|
let position = 0;
|
||||||
if (
|
let services: any = [];
|
||||||
!$status.application.isExited &&
|
let selectedService: any = null;
|
||||||
!$status.application.isRestarting &&
|
let noContainer = false;
|
||||||
!$status.application.isRunning
|
|
||||||
) {
|
|
||||||
goto(`/applications/${$page.params.id}/`, { replaceState: true });
|
|
||||||
}
|
|
||||||
const { id } = $page.params;
|
const { id } = $page.params;
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const response = await get(`/applications/${id}`);
|
const response = await get(`/applications/${id}`);
|
||||||
application = response.application;
|
application = response.application;
|
||||||
loadAllLogs();
|
if (response.application.dockerComposeFile) {
|
||||||
loadLogsInterval = setInterval(() => {
|
services = normalizeDockerServices(
|
||||||
loadLogs();
|
JSON.parse(response.application.dockerComposeFile).services
|
||||||
}, 1000);
|
);
|
||||||
|
} else {
|
||||||
|
services = [
|
||||||
|
{
|
||||||
|
name: ''
|
||||||
|
}
|
||||||
|
];
|
||||||
|
await selectService('');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
clearInterval(loadLogsInterval);
|
clearInterval(loadLogsInterval);
|
||||||
clearInterval(followingInterval);
|
clearInterval(followingInterval);
|
||||||
});
|
});
|
||||||
async function loadAllLogs() {
|
function normalizeDockerServices(services: any[]) {
|
||||||
try {
|
const tempdockerComposeServices = [];
|
||||||
logsLoading = true;
|
for (const [name, data] of Object.entries(services)) {
|
||||||
const data: any = await get(`/applications/${id}/logs`);
|
tempdockerComposeServices.push({
|
||||||
if (data?.logs) {
|
name,
|
||||||
lastLog = data.logs[data.logs.length - 1];
|
data
|
||||||
logs = data.logs;
|
});
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return errorNotification(error);
|
|
||||||
} finally {
|
|
||||||
logsLoading = false;
|
|
||||||
}
|
}
|
||||||
|
return tempdockerComposeServices;
|
||||||
}
|
}
|
||||||
async function loadLogs() {
|
async function loadLogs() {
|
||||||
if (logsLoading) return;
|
if (logsLoading) return;
|
||||||
try {
|
try {
|
||||||
const newLogs: any = await get(
|
const newLogs: any = await get(
|
||||||
`/applications/${id}/logs?since=${lastLog?.split(' ')[0] || 0}`
|
`/applications/${id}/logs/${selectedService}?since=${lastLog?.split(' ')[0] || 0}`
|
||||||
);
|
);
|
||||||
|
if (newLogs.noContainer) {
|
||||||
|
noContainer = true;
|
||||||
|
} else {
|
||||||
|
noContainer = false;
|
||||||
|
}
|
||||||
if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) {
|
if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) {
|
||||||
logs = logs.concat(newLogs.logs);
|
logs = logs.concat(newLogs.logs);
|
||||||
lastLog = newLogs.logs[newLogs.logs.length - 1];
|
lastLog = newLogs.logs[newLogs.logs.length - 1];
|
||||||
@ -89,6 +92,22 @@
|
|||||||
clearInterval(followingInterval);
|
clearInterval(followingInterval);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async function selectService(service: any, init: boolean = false) {
|
||||||
|
if (services.length === 1 && init) return;
|
||||||
|
|
||||||
|
if (loadLogsInterval) clearInterval(loadLogsInterval);
|
||||||
|
if (followingInterval) clearInterval(followingInterval);
|
||||||
|
|
||||||
|
logs = [];
|
||||||
|
lastLog = null;
|
||||||
|
followingLogs = false;
|
||||||
|
|
||||||
|
selectedService = `${application.id}${service.name ? `-${service.name}` : ''}`;
|
||||||
|
loadLogs();
|
||||||
|
loadLogsInterval = setInterval(() => {
|
||||||
|
loadLogs();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-auto w-full">
|
<div class="mx-auto w-full">
|
||||||
@ -96,9 +115,27 @@
|
|||||||
<div class="title font-bold pb-3">Application Logs</div>
|
<div class="title font-bold pb-3">Application Logs</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex 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">
|
<div class="flex flex-row justify-center space-x-2">
|
||||||
{#if logs.length === 0}
|
{#if logs.length === 0}
|
||||||
<div class="text-xl font-bold tracking-tighter">{$t('application.build.waiting_logs')}</div>
|
{#if noContainer}
|
||||||
|
<div class="text-xl font-bold tracking-tighter">Container not found / exited.</div>
|
||||||
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="relative w-full">
|
<div class="relative w-full">
|
||||||
<div class="flex justify-start sticky space-x-2 pb-2">
|
<div class="flex justify-start sticky space-x-2 pb-2">
|
||||||
@ -143,3 +180,4 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
export let application: any;
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { get } from '$lib/api';
|
import { get } from '$lib/api';
|
||||||
import { status } from '$lib/store';
|
import { status } from '$lib/store';
|
||||||
|
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||||
|
|
||||||
const { id } = $page.params;
|
const { id } = $page.params;
|
||||||
|
let services: any = [];
|
||||||
|
let selectedService: any = null;
|
||||||
let usageLoading = false;
|
let usageLoading = false;
|
||||||
let usage = {
|
let usage = {
|
||||||
MemUsage: 0,
|
MemUsage: 0,
|
||||||
@ -16,20 +19,55 @@
|
|||||||
|
|
||||||
async function getUsage() {
|
async function getUsage() {
|
||||||
if (usageLoading) return;
|
if (usageLoading) return;
|
||||||
if (!$status.application.isRunning) return;
|
|
||||||
usageLoading = true;
|
usageLoading = true;
|
||||||
const data = await get(`/applications/${id}/usage`);
|
const data = await get(`/applications/${id}/usage/${selectedService}`);
|
||||||
usage = data.usage;
|
usage = data.usage;
|
||||||
usageLoading = false;
|
usageLoading = false;
|
||||||
}
|
}
|
||||||
|
function normalizeDockerServices(services: any[]) {
|
||||||
|
const tempdockerComposeServices = [];
|
||||||
|
for (const [name, data] of Object.entries(services)) {
|
||||||
|
tempdockerComposeServices.push({
|
||||||
|
name,
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return tempdockerComposeServices;
|
||||||
|
}
|
||||||
|
async function selectService(service: any, init: boolean = false) {
|
||||||
|
if (services.length === 1 && init) return;
|
||||||
|
if (usageInterval) clearInterval(usageInterval);
|
||||||
|
usageLoading = false;
|
||||||
|
usage = {
|
||||||
|
MemUsage: 0,
|
||||||
|
CPUPerc: 0,
|
||||||
|
NetIO: 0
|
||||||
|
};
|
||||||
|
selectedService = `${application.id}${service.name ? `-${service.name}` : ''}`;
|
||||||
|
|
||||||
|
await getUsage();
|
||||||
|
usageInterval = setInterval(async () => {
|
||||||
|
await getUsage();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
clearInterval(usageInterval);
|
clearInterval(usageInterval);
|
||||||
});
|
});
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await getUsage();
|
const response = await get(`/applications/${id}`);
|
||||||
usageInterval = setInterval(async () => {
|
application = response.application;
|
||||||
await getUsage();
|
if (response.application.dockerComposeFile) {
|
||||||
}, 1000);
|
services = normalizeDockerServices(
|
||||||
|
JSON.parse(response.application.dockerComposeFile).services
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
services = [
|
||||||
|
{
|
||||||
|
name: ''
|
||||||
|
}
|
||||||
|
];
|
||||||
|
await selectService('');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -38,7 +76,29 @@
|
|||||||
<div class="title font-bold pb-3">Monitoring</div>
|
<div class="title font-bold pb-3">Monitoring</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mx-auto max-w-4xl px-6 py-4">
|
<div class="flex gap-2 lg:gap-8 pb-4">
|
||||||
|
{#each services as service}
|
||||||
|
<button
|
||||||
|
on:click={() => selectService(service, true)}
|
||||||
|
class:bg-primary={selectedService ===
|
||||||
|
`${application.id}${service.name ? `-${service.name}` : ''}`}
|
||||||
|
class:bg-coolgray-200={selectedService !==
|
||||||
|
`${application.id}${service.name ? `-${service.name}` : ''}`}
|
||||||
|
class="w-full rounded p-5 hover:bg-primary font-bold"
|
||||||
|
>
|
||||||
|
{application.id}{service.name ? `-${service.name}` : ''}</button
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{#if selectedService}
|
||||||
|
<div class="mx-auto max-w-4xl px-6 py-4 bg-coolgray-100 border border-coolgray-200 relative">
|
||||||
|
{#if usageLoading}
|
||||||
|
<button
|
||||||
|
id="streaming"
|
||||||
|
class="btn btn-sm bg-transparent border-none loading absolute top-0 left-0 text-xs"
|
||||||
|
/>
|
||||||
|
<Tooltip triggeredBy="#streaming">Streaming logs</Tooltip>
|
||||||
|
{/if}
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="stat w-64">
|
<div class="stat w-64">
|
||||||
<div class="stat-title">Used Memory / Memory Limit</div>
|
<div class="stat-title">Used Memory / Memory Limit</div>
|
||||||
@ -56,3 +116,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
@ -21,9 +21,11 @@
|
|||||||
|
|
||||||
const { id } = $page.params;
|
const { id } = $page.params;
|
||||||
|
|
||||||
let loading = false;
|
let loading = {
|
||||||
let publicLoading = false;
|
main: false,
|
||||||
|
public: false
|
||||||
|
};
|
||||||
|
let publicUrl = '';
|
||||||
let appendOnly = database.settings.appendOnly;
|
let appendOnly = database.settings.appendOnly;
|
||||||
|
|
||||||
let databaseDefault: any;
|
let databaseDefault: any;
|
||||||
@ -47,23 +49,46 @@
|
|||||||
databaseDbUser = '';
|
databaseDbUser = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function generateUrl(): string {
|
function generateUrl() {
|
||||||
return `${database.type}://${
|
const ipAddress = () => {
|
||||||
databaseDbUser ? databaseDbUser + ':' : ''
|
if ($status.database.isPublic) {
|
||||||
}${databaseDbUserPassword}@${
|
if (database.destinationDocker.remoteEngine) {
|
||||||
$status.database.isPublic
|
return database.destinationDocker.remoteIpAddress;
|
||||||
? database.destinationDocker.remoteEngine
|
}
|
||||||
? database.destinationDocker.remoteIpAddress
|
if ($appSession.ipv6) {
|
||||||
: $appSession.ipv4
|
return $appSession.ipv6;
|
||||||
: database.id
|
}
|
||||||
}:${$status.database.isPublic ? database.publicPort : privatePort}/${databaseDefault}`;
|
if ($appSession.ipv4) {
|
||||||
|
return $appSession.ipv4;
|
||||||
|
}
|
||||||
|
return '<Cannot determine public IP address>';
|
||||||
|
} else {
|
||||||
|
return database.id;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const user = () => {
|
||||||
|
if (databaseDbUser) {
|
||||||
|
return databaseDbUser + ':';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
const port = () => {
|
||||||
|
if ($status.database.isPublic) {
|
||||||
|
return database.publicPort;
|
||||||
|
} else {
|
||||||
|
return privatePort;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
publicUrl = `${
|
||||||
|
database.type
|
||||||
|
}://${user()}${databaseDbUserPassword}@${ipAddress()}:${port()}/${databaseDefault}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function changeSettings(name: any) {
|
async function changeSettings(name: any) {
|
||||||
if (name !== 'appendOnly') {
|
if (name !== 'appendOnly') {
|
||||||
if (publicLoading || !$status.database.isRunning) return;
|
if (loading.public || !$status.database.isRunning) return;
|
||||||
}
|
}
|
||||||
publicLoading = true;
|
loading.public = true;
|
||||||
let data = {
|
let data = {
|
||||||
isPublic: $status.database.isPublic,
|
isPublic: $status.database.isPublic,
|
||||||
appendOnly
|
appendOnly
|
||||||
@ -87,12 +112,12 @@
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorNotification(error);
|
return errorNotification(error);
|
||||||
} finally {
|
} finally {
|
||||||
publicLoading = false;
|
loading.public = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
try {
|
try {
|
||||||
loading = true;
|
loading.main = true;
|
||||||
await post(`/databases/${id}`, { ...database, isRunning: $status.database.isRunning });
|
await post(`/databases/${id}`, { ...database, isRunning: $status.database.isRunning });
|
||||||
generateDbDetails();
|
generateDbDetails();
|
||||||
addToast({
|
addToast({
|
||||||
@ -102,7 +127,7 @@
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorNotification(error);
|
return errorNotification(error);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading.main = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -115,9 +140,9 @@
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="btn btn-sm"
|
class="btn btn-sm"
|
||||||
class:loading
|
class:loading={loading.main}
|
||||||
class:bg-databases={!loading}
|
class:bg-databases={!loading.main}
|
||||||
disabled={loading}>{$t('forms.save')}</button
|
disabled={loading.main}>{$t('forms.save')}</button
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@ -175,7 +200,7 @@
|
|||||||
readonly
|
readonly
|
||||||
disabled
|
disabled
|
||||||
name="publicPort"
|
name="publicPort"
|
||||||
value={publicLoading
|
value={loading.public
|
||||||
? 'Loading...'
|
? 'Loading...'
|
||||||
: $status.database.isPublic
|
: $status.database.isPublic
|
||||||
? database.publicPort
|
? database.publicPort
|
||||||
@ -198,8 +223,8 @@
|
|||||||
<EdgeDB {database} />
|
<EdgeDB {database} />
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex flex-col space-y-2 mt-5">
|
<div class="flex flex-col space-y-2 mt-5">
|
||||||
<div>
|
<div class="grid gap-2 grid-cols-2 auto-rows-max lg:px-10 px-2">
|
||||||
<label class="px-2" for="url"
|
<label for="url"
|
||||||
>{$t('database.connection_string')}
|
>{$t('database.connection_string')}
|
||||||
{#if !$status.database.isPublic && database.destinationDocker.remoteEngine}
|
{#if !$status.database.isPublic && database.destinationDocker.remoteEngine}
|
||||||
<Explainer
|
<Explainer
|
||||||
@ -207,18 +232,21 @@
|
|||||||
/>
|
/>
|
||||||
{/if}</label
|
{/if}</label
|
||||||
>
|
>
|
||||||
|
<button class="btn btn-sm" on:click|preventDefault={generateUrl}
|
||||||
|
>Show Connection String</button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="lg:px-10 px-2">
|
<div class="lg:px-10 px-2">
|
||||||
|
{#if publicUrl}
|
||||||
<CopyPasswordField
|
<CopyPasswordField
|
||||||
textarea={true}
|
placeholder="Click on the button to generate URL"
|
||||||
placeholder={$t('forms.generated_automatically_after_start')}
|
|
||||||
isPasswordField={false}
|
|
||||||
id="url"
|
id="url"
|
||||||
name="url"
|
name="url"
|
||||||
readonly
|
readonly
|
||||||
disabled
|
disabled
|
||||||
value={publicLoading || loading ? 'Loading...' : generateUrl()}
|
value={loading.public ? 'Loading...' : publicUrl}
|
||||||
/>
|
/>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@ -228,7 +256,7 @@
|
|||||||
<div class="grid gap-2 grid-cols-2 auto-rows-max lg:px-10 px-2">
|
<div class="grid gap-2 grid-cols-2 auto-rows-max lg:px-10 px-2">
|
||||||
<Setting
|
<Setting
|
||||||
id="isPublic"
|
id="isPublic"
|
||||||
loading={publicLoading}
|
loading={loading.public}
|
||||||
bind:setting={$status.database.isPublic}
|
bind:setting={$status.database.isPublic}
|
||||||
on:click={() => changeSettings('isPublic')}
|
on:click={() => changeSettings('isPublic')}
|
||||||
title={$t('database.set_public')}
|
title={$t('database.set_public')}
|
||||||
@ -238,7 +266,7 @@
|
|||||||
{#if database.type === 'redis'}
|
{#if database.type === 'redis'}
|
||||||
<Setting
|
<Setting
|
||||||
id="appendOnly"
|
id="appendOnly"
|
||||||
loading={publicLoading}
|
loading={loading.public}
|
||||||
bind:setting={appendOnly}
|
bind:setting={appendOnly}
|
||||||
on:click={() => changeSettings('appendOnly')}
|
on:click={() => changeSettings('appendOnly')}
|
||||||
title={$t('database.change_append_only_mode')}
|
title={$t('database.change_append_only_mode')}
|
||||||
|
@ -146,9 +146,29 @@
|
|||||||
try {
|
try {
|
||||||
numberOfGetStatus++;
|
numberOfGetStatus++;
|
||||||
let isRunning = false;
|
let isRunning = false;
|
||||||
|
let isDegraded = false;
|
||||||
if (buildPack) {
|
if (buildPack) {
|
||||||
const response = await get(`/applications/${id}/status`);
|
const response = await get(`/applications/${id}/status`);
|
||||||
isRunning = response.isRunning;
|
if (response.length === 0) {
|
||||||
|
isRunning = false;
|
||||||
|
} else if (response.length === 1) {
|
||||||
|
isRunning = response[0].status.isRunning;
|
||||||
|
} else {
|
||||||
|
let overallStatus = false;
|
||||||
|
for (const oneStatus of response) {
|
||||||
|
if (oneStatus.status.isRunning) {
|
||||||
|
overallStatus = true;
|
||||||
|
} else {
|
||||||
|
isDegraded = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (overallStatus) {
|
||||||
|
isRunning = true;
|
||||||
|
} else {
|
||||||
|
isRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (typeof dualCerts !== 'undefined') {
|
} else if (typeof dualCerts !== 'undefined') {
|
||||||
const response = await get(`/services/${id}/status`);
|
const response = await get(`/services/${id}/status`);
|
||||||
isRunning = response.isRunning;
|
isRunning = response.isRunning;
|
||||||
@ -156,9 +176,13 @@
|
|||||||
const response = await get(`/databases/${id}/status`);
|
const response = await get(`/databases/${id}/status`);
|
||||||
isRunning = response.isRunning;
|
isRunning = response.isRunning;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
status[id] = 'running';
|
status[id] = 'running';
|
||||||
return 'running';
|
return 'running';
|
||||||
|
} else if (isDegraded) {
|
||||||
|
status[id] = 'degraded';
|
||||||
|
return 'degraded';
|
||||||
} else {
|
} else {
|
||||||
status[id] = 'stopped';
|
status[id] = 'stopped';
|
||||||
return 'stopped';
|
return 'stopped';
|
||||||
@ -213,6 +237,7 @@
|
|||||||
(application.id && application.id.toLowerCase().includes($search.toLowerCase())) ||
|
(application.id && application.id.toLowerCase().includes($search.toLowerCase())) ||
|
||||||
(application.name && application.name.toLowerCase().includes($search.toLowerCase())) ||
|
(application.name && application.name.toLowerCase().includes($search.toLowerCase())) ||
|
||||||
(application.fqdn && application.fqdn.toLowerCase().includes($search.toLowerCase())) ||
|
(application.fqdn && application.fqdn.toLowerCase().includes($search.toLowerCase())) ||
|
||||||
|
(application.dockerComposeConfiguration && application.dockerComposeConfiguration.toLowerCase().includes($search.toLowerCase())) ||
|
||||||
(application.repository &&
|
(application.repository &&
|
||||||
application.repository.toLowerCase().includes($search.toLowerCase())) ||
|
application.repository.toLowerCase().includes($search.toLowerCase())) ||
|
||||||
(application.buildpack &&
|
(application.buildpack &&
|
||||||
@ -594,6 +619,11 @@
|
|||||||
<span class="indicator-item badge bg-yellow-300 badge-sm" />
|
<span class="indicator-item badge bg-yellow-300 badge-sm" />
|
||||||
{:else if status[application.id] === 'running'}
|
{:else if status[application.id] === 'running'}
|
||||||
<span class="indicator-item badge bg-success badge-sm" />
|
<span class="indicator-item badge bg-success badge-sm" />
|
||||||
|
{:else if status[application.id] === 'degraded'}
|
||||||
|
<span
|
||||||
|
class="indicator-item indicator-middle indicator-center badge bg-warning text-black font-bold badge-xl"
|
||||||
|
>Degraded</span
|
||||||
|
>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="indicator-item badge bg-error badge-sm" />
|
<span class="indicator-item badge bg-error badge-sm" />
|
||||||
{/if}
|
{/if}
|
||||||
@ -613,7 +643,7 @@
|
|||||||
<div class="h-10 text-xs">
|
<div class="h-10 text-xs">
|
||||||
{#if application?.fqdn}
|
{#if application?.fqdn}
|
||||||
<h2>{application?.fqdn.replace('https://', '').replace('http://', '')}</h2>
|
<h2>{application?.fqdn.replace('https://', '').replace('http://', '')}</h2>
|
||||||
{:else if !application.settings?.isBot && !application?.fqdn}
|
{:else if (!application.settings?.isBot && !application?.fqdn) && application.buildPack !== 'compose'}
|
||||||
<h2 class="text-red-500">Not configured</h2>
|
<h2 class="text-red-500">Not configured</h2>
|
||||||
{/if}
|
{/if}
|
||||||
{#if application.destinationDocker?.name}
|
{#if application.destinationDocker?.name}
|
||||||
|
@ -27,10 +27,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let types: any;
|
export let types: any;
|
||||||
|
|
||||||
|
let search = '';
|
||||||
|
let filteredTypes = types;
|
||||||
|
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { get, post } from '$lib/api';
|
import { get, post } from '$lib/api';
|
||||||
import { t } from '$lib/translations';
|
|
||||||
import { errorNotification } from '$lib/common';
|
import { errorNotification } from '$lib/common';
|
||||||
import ServiceIcons from '$lib/components/svg/services/ServiceIcons.svelte';
|
import ServiceIcons from '$lib/components/svg/services/ServiceIcons.svelte';
|
||||||
|
|
||||||
@ -45,10 +47,50 @@
|
|||||||
return errorNotification(error);
|
return errorNotification(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function doSearch() {
|
||||||
|
filteredTypes = types.filter(
|
||||||
|
(type: any) =>
|
||||||
|
type.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||||
|
type.labels.some((label: string) => label.toLowerCase().includes(search.toLowerCase()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function cleanupSearch() {
|
||||||
|
search = '';
|
||||||
|
filteredTypes = types;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-wrap justify-center">
|
<div class="container lg:mx-auto lg:p-0 px-8 pt-5">
|
||||||
{#each types as type}
|
<div class="input-group flex w-full">
|
||||||
|
<div class="btn btn-square cursor-default no-animation hover:bg-error" on:click={cleanupSearch}>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="w-6 h-6"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentcolor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18" />
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
id="search"
|
||||||
|
class="input w-full"
|
||||||
|
type="text"
|
||||||
|
placeholder="Search for services"
|
||||||
|
bind:value={search}
|
||||||
|
on:input={() => doSearch()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container lg:mx-auto lg:pt-20 lg:p-0 px-8 pt-20">
|
||||||
|
<div class="flex flex-wrap justify-center gap-8">
|
||||||
|
{#each filteredTypes as type}
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<form on:submit|preventDefault={() => handleSubmit(type.name)}>
|
<form on:submit|preventDefault={() => handleSubmit(type.name)}>
|
||||||
<button type="submit" class="box-selection relative text-xl font-bold hover:bg-pink-600">
|
<button type="submit" class="box-selection relative text-xl font-bold hover:bg-pink-600">
|
||||||
@ -59,3 +101,4 @@
|
|||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
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-100": "#323232",
|
||||||
"base-200": "#242424",
|
"base-200": "#242424",
|
||||||
"base-300": "#181818",
|
"base-300": "#181818",
|
||||||
"primary": "#6d28d9",
|
"primary": "#6B16ED",
|
||||||
"primary-content": "#fff",
|
"primary-content": "#fff",
|
||||||
"secondary": "#343232",
|
"secondary": "#343232",
|
||||||
"accent": "#343232",
|
"accent": "#343232",
|
||||||
|
@ -1,6 +1,38 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
coolify:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile-dev
|
||||||
|
command: bash -c 'pnpm install && pnpm db:push && pnpm db:seed && pnpm dev'
|
||||||
|
environment:
|
||||||
|
- COOLIFY_CONTAINER_DEV=true
|
||||||
|
- COOLIFY_APP_ID=random-local-id
|
||||||
|
- COOLIFY_SECRET_KEY=12341234123412341234123412341234
|
||||||
|
- COOLIFY_DATABASE_URL=file:../db/dev.db
|
||||||
|
- GITPOD_WORKSPACE_URL=${GITPOD_WORKSPACE_URL}
|
||||||
|
- CODESANDBOX_HOST=${CODESANDBOX_HOST}
|
||||||
|
container_name: coolify
|
||||||
|
ports:
|
||||||
|
- target: 3000
|
||||||
|
published: 3000
|
||||||
|
protocol: tcp
|
||||||
|
mode: host
|
||||||
|
- target: 3001
|
||||||
|
published: 3001
|
||||||
|
protocol: tcp
|
||||||
|
mode: host
|
||||||
|
- target: 5555
|
||||||
|
published: 5555
|
||||||
|
protocol: tcp
|
||||||
|
mode: host
|
||||||
|
volumes:
|
||||||
|
- ./:/app
|
||||||
|
- '/var/run/docker.sock:/var/run/docker.sock'
|
||||||
|
- /tmp:/tmp
|
||||||
|
networks:
|
||||||
|
- coolify-infra
|
||||||
fluent-bit:
|
fluent-bit:
|
||||||
image: coollabsio/coolify-fluent-bit:1.0.0
|
image: coollabsio/coolify-fluent-bit:1.0.0
|
||||||
command: /fluent-bit/bin/fluent-bit -c /fluent-bit/etc/fluent-bit-dev.conf
|
command: /fluent-bit/bin/fluent-bit -c /fluent-bit/etc/fluent-bit-dev.conf
|
||||||
@ -8,7 +40,10 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./logs:/logs
|
- ./logs:/logs
|
||||||
ports:
|
ports:
|
||||||
- "24224:24224"
|
- target: 24224
|
||||||
|
published: 24224
|
||||||
|
protocol: tcp
|
||||||
|
mode: host
|
||||||
networks:
|
networks:
|
||||||
- coolify-infra
|
- coolify-infra
|
||||||
networks:
|
networks:
|
||||||
|
@ -1,21 +1,24 @@
|
|||||||
{
|
{
|
||||||
"name": "coolify",
|
"name": "coolify",
|
||||||
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
|
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
|
||||||
"version": "3.10.14",
|
"version": "3.10.15",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"repository": "github:coollabsio/coolify",
|
"repository": "github:coollabsio/coolify",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"oc": "opencollective-setup",
|
"oc": "opencollective-setup",
|
||||||
"translate": "pnpm run --filter i18n-converter translate",
|
"translate": "pnpm run --filter i18n-converter translate",
|
||||||
"db:studio": "pnpm run --filter api db:studio",
|
"db:studio": "pnpm run --filter api db:studio",
|
||||||
|
"db:studio:container": "docker exec coolify pnpm run --filter api db:studio",
|
||||||
"db:push": "pnpm run --filter api db:push",
|
"db:push": "pnpm run --filter api db:push",
|
||||||
"db:seed": "pnpm run --filter api db:seed",
|
"db:seed": "pnpm run --filter api db:seed",
|
||||||
"db:migrate": "pnpm run --filter api db:migrate",
|
"db:migrate": "pnpm run --filter api db:migrate",
|
||||||
|
"db:migrate:container": "docker exec coolify pnpm run --filter api db:migrate",
|
||||||
"format": "run-p -l -n format:*",
|
"format": "run-p -l -n format:*",
|
||||||
"format:api": "NODE_ENV=development pnpm run --filter api format",
|
"format:api": "NODE_ENV=development pnpm run --filter api format",
|
||||||
"lint": "run-p -l -n lint:*",
|
"lint": "run-p -l -n lint:*",
|
||||||
"lint:api": "NODE_ENV=development pnpm run --filter api lint",
|
"lint:api": "NODE_ENV=development pnpm run --filter api lint",
|
||||||
"dev": "run-p -l -n dev:*",
|
"dev:container": "docker-compose -f docker-compose-dev.yaml up --build || docker compose -f docker-compose-dev.yaml up --build",
|
||||||
|
"dev": "run-p -l -n dev:api dev:ui",
|
||||||
"dev:api": "NODE_ENV=development pnpm run --filter api dev",
|
"dev:api": "NODE_ENV=development pnpm run --filter api dev",
|
||||||
"dev:ui": "NODE_ENV=development pnpm run --filter ui dev",
|
"dev:ui": "NODE_ENV=development pnpm run --filter ui dev",
|
||||||
"build": "NODE_ENV=production run-p -n build:*",
|
"build": "NODE_ENV=production run-p -n build:*",
|
||||||
@ -34,6 +37,7 @@
|
|||||||
"docker",
|
"docker",
|
||||||
"self-host",
|
"self-host",
|
||||||
"iaas",
|
"iaas",
|
||||||
|
"paas",
|
||||||
"heroku",
|
"heroku",
|
||||||
"netlify",
|
"netlify",
|
||||||
"open-source",
|
"open-source",
|
||||||
|
@ -104,6 +104,7 @@ importers:
|
|||||||
node-os-utils: 1.3.7
|
node-os-utils: 1.3.7
|
||||||
p-all: 4.0.0
|
p-all: 4.0.0
|
||||||
p-throttle: 5.0.0
|
p-throttle: 5.0.0
|
||||||
|
prisma: 4.4.0
|
||||||
public-ip: 6.0.1
|
public-ip: 6.0.1
|
||||||
pump: 3.0.0
|
pump: 3.0.0
|
||||||
ssh-config: 4.1.6
|
ssh-config: 4.1.6
|
||||||
@ -120,7 +121,6 @@ importers:
|
|||||||
eslint-plugin-prettier: 4.2.1_tgumt6uwl2md3n6uqnggd6wvce
|
eslint-plugin-prettier: 4.2.1_tgumt6uwl2md3n6uqnggd6wvce
|
||||||
nodemon: 2.0.20
|
nodemon: 2.0.20
|
||||||
prettier: 2.7.1
|
prettier: 2.7.1
|
||||||
prisma: 4.4.0
|
|
||||||
rimraf: 3.0.2
|
rimraf: 3.0.2
|
||||||
tsconfig-paths: 4.1.0
|
tsconfig-paths: 4.1.0
|
||||||
typescript: 4.8.4
|
typescript: 4.8.4
|
||||||
@ -159,6 +159,7 @@ importers:
|
|||||||
flowbite: 1.5.2
|
flowbite: 1.5.2
|
||||||
flowbite-svelte: 0.26.2
|
flowbite-svelte: 0.26.2
|
||||||
js-cookie: 3.0.1
|
js-cookie: 3.0.1
|
||||||
|
js-yaml: 4.1.0
|
||||||
p-limit: 4.0.0
|
p-limit: 4.0.0
|
||||||
postcss: 8.4.16
|
postcss: 8.4.16
|
||||||
prettier: 2.7.1
|
prettier: 2.7.1
|
||||||
@ -181,6 +182,7 @@ importers:
|
|||||||
daisyui: 2.24.2_25hquoklqeoqwmt7fwvvcyxm5e
|
daisyui: 2.24.2_25hquoklqeoqwmt7fwvvcyxm5e
|
||||||
dayjs: 1.11.5
|
dayjs: 1.11.5
|
||||||
js-cookie: 3.0.1
|
js-cookie: 3.0.1
|
||||||
|
js-yaml: 4.1.0
|
||||||
p-limit: 4.0.0
|
p-limit: 4.0.0
|
||||||
svelte-file-dropzone: 1.0.0
|
svelte-file-dropzone: 1.0.0
|
||||||
svelte-select: 4.4.7
|
svelte-select: 4.4.7
|
||||||
@ -532,6 +534,7 @@ packages:
|
|||||||
/@prisma/engines/4.4.0:
|
/@prisma/engines/4.4.0:
|
||||||
resolution: {integrity: sha512-Fpykccxlt9MHrAs/QpPGpI2nOiRxuLA+LiApgA59ibbf24YICZIMWd3SI2YD+q0IAIso0jCGiHhirAIbxK3RyQ==}
|
resolution: {integrity: sha512-Fpykccxlt9MHrAs/QpPGpI2nOiRxuLA+LiApgA59ibbf24YICZIMWd3SI2YD+q0IAIso0jCGiHhirAIbxK3RyQ==}
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@rollup/pluginutils/4.2.1:
|
/@rollup/pluginutils/4.2.1:
|
||||||
resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==}
|
resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==}
|
||||||
@ -5078,6 +5081,7 @@ packages:
|
|||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@prisma/engines': 4.4.0
|
'@prisma/engines': 4.4.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/private/0.1.8:
|
/private/0.1.8:
|
||||||
resolution: {integrity: sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==}
|
resolution: {integrity: sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==}
|
||||||
|
Loading…
Reference in New Issue
Block a user