mirror of
https://github.com/ershisan99/coolify.git
synced 2025-12-31 20:59:23 +00:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bd9f00268 | ||
|
|
1aadda735d | ||
|
|
12035208e2 | ||
|
|
df8a9f673c | ||
|
|
aa5c8a2c56 | ||
|
|
a84540e6bb | ||
|
|
fb91b64063 | ||
|
|
94cc77ebca | ||
|
|
aac6981304 | ||
|
|
ca05828b68 | ||
|
|
8ec6b4c59c | ||
|
|
f1be5f5341 | ||
|
|
714c264002 | ||
|
|
eca58097ef | ||
|
|
281146e22b | ||
|
|
f3a19a5d02 | ||
|
|
9b9b6937f4 | ||
|
|
f54c0b7dff | ||
|
|
36c58ad286 | ||
|
|
a67f633259 | ||
|
|
f39a607c1a | ||
|
|
0cc67ed2e5 | ||
|
|
5f8402c645 | ||
|
|
3ab87cd11e | ||
|
|
d5620d305d | ||
|
|
35ebc5e842 | ||
|
|
66276be1d2 | ||
|
|
47c0d522db | ||
|
|
b654883d1a | ||
|
|
b4f9d29129 | ||
|
|
bec6b961f3 | ||
|
|
2ce8f34306 | ||
|
|
30d1ae59ec | ||
|
|
ac7d4e3645 | ||
|
|
868c4001f6 | ||
|
|
e99c44d967 |
93
.github/workflows/fluent-bit-release.yml
vendored
Normal file
93
.github/workflows/fluent-bit-release.yml
vendored
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
name: fluent-bit-release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "others/fluentbit"
|
||||||
|
- ".github/workflows/fluent-bit-release.yml"
|
||||||
|
branches:
|
||||||
|
- next
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
arm64:
|
||||||
|
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: Build and push
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: others/fluentbit/
|
||||||
|
platforms: linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: coollabsio/coolify-fluent-bit:1.0.0-arm64
|
||||||
|
amd64:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: others/fluentbit/
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: true
|
||||||
|
tags: coollabsio/coolify-fluent-bit:1.0.0-amd64
|
||||||
|
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: Build and push
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: others/fluentbit/
|
||||||
|
platforms: linux/aarch64
|
||||||
|
push: true
|
||||||
|
tags: coollabsio/coolify-fluent-bit:1.0.0-aarch64
|
||||||
|
merge-manifest:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [amd64, arm64, aarch64]
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Create & publish manifest
|
||||||
|
run: |
|
||||||
|
docker manifest create coollabsio/coolify-fluent-bit:1.0.0 --amend coollabsio/coolify-fluent-bit:1.0.0-amd64 --amend coollabsio/coolify-fluent-bit:1.0.0-arm64 --amend coollabsio/coolify-fluent-bit:1.0.0-aarch64
|
||||||
|
docker manifest push coollabsio/coolify-fluent-bit:1.0.0
|
||||||
4
.github/workflows/staging-release.yml
vendored
4
.github/workflows/staging-release.yml
vendored
@@ -2,6 +2,10 @@ name: staging-release
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
paths:
|
||||||
|
- '**'
|
||||||
|
- "!others/fluentbit"
|
||||||
|
- "!.github/workflows/fluent-bit-release.yml"
|
||||||
branches:
|
branches:
|
||||||
- next
|
- next
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,156 @@
|
|||||||
|
- templateVersion: 1.0.0
|
||||||
|
ignore: true
|
||||||
|
defaultVersion: "1.17"
|
||||||
|
documentation: https://docs.gitea.io
|
||||||
|
type: gitea
|
||||||
|
name: Gitea
|
||||||
|
description: Gitea is a community managed lightweight code hosting solution written in Go.
|
||||||
|
labels:
|
||||||
|
- storage
|
||||||
|
- git
|
||||||
|
services:
|
||||||
|
$$id:
|
||||||
|
name: Gitea
|
||||||
|
documentation: https://docs.gitea.io
|
||||||
|
image: gitea/gitea:$$core_version
|
||||||
|
volumes:
|
||||||
|
- $$id-data:/data
|
||||||
|
- /etc/timezone:/etc/timezone:ro
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
environment:
|
||||||
|
- USER_UID=1000
|
||||||
|
- USER_GID=1000
|
||||||
|
- DOMAIN=$$config_domain
|
||||||
|
- SSH_DOMAIN=$$config_ssh_domain
|
||||||
|
- ROOT_URL=$$config_root_url
|
||||||
|
- SECRET_KEY=$$secret_secret_key
|
||||||
|
- INTERNAL_TOKEN=$$secret_internal_token
|
||||||
|
- SSH_PORT=$$config_hostport_ssh
|
||||||
|
ports:
|
||||||
|
- "3000"
|
||||||
|
- "22"
|
||||||
|
proxy:
|
||||||
|
- port: "22"
|
||||||
|
hostPort: $$config_hostport_ssh
|
||||||
|
variables:
|
||||||
|
- id: $$config_hostport_ssh
|
||||||
|
name: SSH_PORT
|
||||||
|
label: SSH Port
|
||||||
|
defaultValue: "8022"
|
||||||
|
description: ""
|
||||||
|
required: true
|
||||||
|
- id: $$config_domain
|
||||||
|
name: DOMAIN
|
||||||
|
label: Domain
|
||||||
|
defaultValue: $$generate_domain
|
||||||
|
description: ""
|
||||||
|
- id: $$config_ssh_domain
|
||||||
|
name: SSH_DOMAIN
|
||||||
|
label: SSH Domain
|
||||||
|
defaultValue: $$generate_domain
|
||||||
|
description: ""
|
||||||
|
- id: $$config_root_url
|
||||||
|
name: ROOT_URL
|
||||||
|
label: Root URL of Gitea
|
||||||
|
defaultValue: $$generate_fqdn_slash
|
||||||
|
description: ""
|
||||||
|
- id: $$secret_secret_key
|
||||||
|
name: SECRET_KEY
|
||||||
|
label: Secret Key
|
||||||
|
defaultValue: $$generate_hex(32)
|
||||||
|
description: ""
|
||||||
|
showOnConfiguration: true
|
||||||
|
- id: $$secret_internal_token
|
||||||
|
name: INTERNAL_TOKEN
|
||||||
|
label: Internal JWT Token
|
||||||
|
defaultValue: $$generate_token
|
||||||
|
description: ""
|
||||||
|
showOnConfiguration: true
|
||||||
|
- templateVersion: 1.0.0
|
||||||
|
defaultVersion: "20.0"
|
||||||
|
documentation: https://www.keycloak.org/documentation
|
||||||
|
type: keycloak
|
||||||
|
name: Keycloak
|
||||||
|
description: "Keycloak provides user federation, strong authentication, user management, fine-grained authorization, and more."
|
||||||
|
labels:
|
||||||
|
- authentication
|
||||||
|
- authorization
|
||||||
|
- oidconnect
|
||||||
|
- saml2
|
||||||
|
services:
|
||||||
|
$$id:
|
||||||
|
name: Keycloak
|
||||||
|
command: start --db=postgres --features=token-exchange --import-realm
|
||||||
|
depends_on:
|
||||||
|
- $$id-postgresql
|
||||||
|
image: "quay.io/keycloak/keycloak:$$core_version"
|
||||||
|
volumes:
|
||||||
|
- $$id-import:/opt/keycloak/data/import
|
||||||
|
environment:
|
||||||
|
- KC_HEALTH_ENABLED=true
|
||||||
|
- KC_PROXY=edge
|
||||||
|
- KC_DB=postgres
|
||||||
|
- KC_HOSTNAME=$$config_keycloak_domain
|
||||||
|
- KEYCLOAK_ADMIN=$$config_admin_user
|
||||||
|
- KEYCLOAK_ADMIN_PASSWORD=$$secret_keycloak_admin_password
|
||||||
|
- KC_DB_PASSWORD=$$secret_postgres_password
|
||||||
|
- KC_DB_USERNAME=$$config_postgres_user
|
||||||
|
- KC_DB_URL=$$secret_keycloak_database_url
|
||||||
|
ports:
|
||||||
|
- "8080"
|
||||||
|
$$id-postgresql:
|
||||||
|
name: PostgreSQL
|
||||||
|
depends_on: []
|
||||||
|
image: "postgres:14-alpine"
|
||||||
|
volumes:
|
||||||
|
- "$$id-postgresql-data:/var/lib/postgresql/data"
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=$$config_postgres_user
|
||||||
|
- POSTGRES_PASSWORD=$$secret_postgres_password
|
||||||
|
- POSTGRES_DB=$$config_postgres_db
|
||||||
|
ports: []
|
||||||
|
variables:
|
||||||
|
- id: $$config_keycloak_domain
|
||||||
|
name: KEYCLOAK_DOMAIN
|
||||||
|
label: Keycloak Domain
|
||||||
|
defaultValue: $$generate_domain
|
||||||
|
description: ""
|
||||||
|
- id: $$secret_keycloak_database_url
|
||||||
|
name: KEYCLOAK_DATABASE_URL
|
||||||
|
label: Keycloak Database Url
|
||||||
|
defaultValue: >-
|
||||||
|
jdbc:postgresql://$$id-postgresql:5432/$$config_postgres_db
|
||||||
|
description: ""
|
||||||
|
- id: $$config_admin_user
|
||||||
|
name: KEYCLOAK_ADMIN
|
||||||
|
label: Keycloak Admin User
|
||||||
|
defaultValue: $$generate_username
|
||||||
|
description: ""
|
||||||
|
- id: $$secret_keycloak_admin_password
|
||||||
|
name: KEYCLOAK_ADMIN_PASSWORD
|
||||||
|
label: Keycloak Admin Password
|
||||||
|
defaultValue: $$generate_password
|
||||||
|
description: ""
|
||||||
|
showOnConfiguration: true
|
||||||
|
- id: $$config_postgres_user
|
||||||
|
main: $$id-postgresql
|
||||||
|
name: POSTGRES_USER
|
||||||
|
label: PostgreSQL User
|
||||||
|
defaultValue: $$generate_username
|
||||||
|
description: ""
|
||||||
|
- id: $$secret_postgres_password
|
||||||
|
main: $$id-postgresql
|
||||||
|
name: POSTGRES_PASSWORD
|
||||||
|
label: PostgreSQL Password
|
||||||
|
defaultValue: $$generate_password
|
||||||
|
description: ""
|
||||||
|
showOnConfiguration: true
|
||||||
|
- id: $$config_postgres_db
|
||||||
|
main: $$id-postgresql
|
||||||
|
name: POSTGRES_DB
|
||||||
|
label: PostgreSQL Database
|
||||||
|
defaultValue: keycloak
|
||||||
|
description: ""
|
||||||
- templateVersion: 1.0.0
|
- templateVersion: 1.0.0
|
||||||
defaultVersion: v3.6
|
defaultVersion: v3.6
|
||||||
documentation: https://github.com/freyacodes/Lavalink
|
documentation: https://github.com/freyacodes/Lavalink
|
||||||
@@ -53,7 +206,7 @@
|
|||||||
- id: $$config_port
|
- id: $$config_port
|
||||||
name: PORT
|
name: PORT
|
||||||
label: Port
|
label: Port
|
||||||
defaultValue: '2333'
|
defaultValue: "2333"
|
||||||
required: true
|
required: true
|
||||||
- id: $$secret_password
|
- id: $$secret_password
|
||||||
name: PASSWORD
|
name: PASSWORD
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
-- RedefineTables
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_Setting" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"fqdn" TEXT,
|
||||||
|
"isAPIDebuggingEnabled" BOOLEAN DEFAULT false,
|
||||||
|
"isRegistrationEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"minPort" INTEGER NOT NULL DEFAULT 9000,
|
||||||
|
"maxPort" INTEGER NOT NULL DEFAULT 9100,
|
||||||
|
"proxyPassword" TEXT NOT NULL,
|
||||||
|
"proxyUser" TEXT NOT NULL,
|
||||||
|
"proxyHash" TEXT,
|
||||||
|
"proxyDefaultRedirect" TEXT,
|
||||||
|
"isAutoUpdateEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"isDNSCheckEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"DNSServers" TEXT,
|
||||||
|
"isTraefikUsed" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
"ipv4" TEXT,
|
||||||
|
"ipv6" TEXT,
|
||||||
|
"arch" TEXT,
|
||||||
|
"concurrentBuilds" INTEGER NOT NULL DEFAULT 1,
|
||||||
|
"applicationStoragePathMigrationFinished" BOOLEAN NOT NULL DEFAULT false
|
||||||
|
);
|
||||||
|
INSERT INTO "new_Setting" ("DNSServers", "arch", "concurrentBuilds", "createdAt", "dualCerts", "fqdn", "id", "ipv4", "ipv6", "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "proxyDefaultRedirect", "proxyHash", "proxyPassword", "proxyUser", "updatedAt") SELECT "DNSServers", "arch", "concurrentBuilds", "createdAt", "dualCerts", "fqdn", "id", "ipv4", "ipv6", "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "proxyDefaultRedirect", "proxyHash", "proxyPassword", "proxyUser", "updatedAt" FROM "Setting";
|
||||||
|
DROP TABLE "Setting";
|
||||||
|
ALTER TABLE "new_Setting" RENAME TO "Setting";
|
||||||
|
CREATE UNIQUE INDEX "Setting_fqdn_key" ON "Setting"("fqdn");
|
||||||
|
CREATE TABLE "new_ApplicationPersistentStorage" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"applicationId" TEXT NOT NULL,
|
||||||
|
"path" TEXT NOT NULL,
|
||||||
|
"oldPath" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "ApplicationPersistentStorage_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO "new_ApplicationPersistentStorage" ("applicationId", "createdAt", "id", "path", "updatedAt") SELECT "applicationId", "createdAt", "id", "path", "updatedAt" FROM "ApplicationPersistentStorage";
|
||||||
|
DROP TABLE "ApplicationPersistentStorage";
|
||||||
|
ALTER TABLE "new_ApplicationPersistentStorage" RENAME TO "ApplicationPersistentStorage";
|
||||||
|
CREATE UNIQUE INDEX "ApplicationPersistentStorage_applicationId_path_key" ON "ApplicationPersistentStorage"("applicationId", "path");
|
||||||
|
PRAGMA foreign_key_check;
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
@@ -19,27 +19,28 @@ model Certificate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Setting {
|
model Setting {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
fqdn String? @unique
|
fqdn String? @unique
|
||||||
isAPIDebuggingEnabled Boolean? @default(false)
|
isAPIDebuggingEnabled Boolean? @default(false)
|
||||||
isRegistrationEnabled Boolean @default(false)
|
isRegistrationEnabled Boolean @default(false)
|
||||||
dualCerts Boolean @default(false)
|
dualCerts Boolean @default(false)
|
||||||
minPort Int @default(9000)
|
minPort Int @default(9000)
|
||||||
maxPort Int @default(9100)
|
maxPort Int @default(9100)
|
||||||
proxyPassword String
|
proxyPassword String
|
||||||
proxyUser String
|
proxyUser String
|
||||||
proxyHash String?
|
proxyHash String?
|
||||||
proxyDefaultRedirect String?
|
proxyDefaultRedirect String?
|
||||||
isAutoUpdateEnabled Boolean @default(false)
|
isAutoUpdateEnabled Boolean @default(false)
|
||||||
isDNSCheckEnabled Boolean @default(true)
|
isDNSCheckEnabled Boolean @default(true)
|
||||||
DNSServers String?
|
DNSServers String?
|
||||||
isTraefikUsed Boolean @default(true)
|
isTraefikUsed Boolean @default(true)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
ipv4 String?
|
ipv4 String?
|
||||||
ipv6 String?
|
ipv6 String?
|
||||||
arch String?
|
arch String?
|
||||||
concurrentBuilds Int @default(1)
|
concurrentBuilds Int @default(1)
|
||||||
|
applicationStoragePathMigrationFinished Boolean @default(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
@@ -186,6 +187,7 @@ model ApplicationPersistentStorage {
|
|||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
applicationId String
|
applicationId String
|
||||||
path String
|
path String
|
||||||
|
oldPath Boolean @default(false)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
application Application @relation(fields: [applicationId], references: [id])
|
application Application @relation(fields: [applicationId], references: [id])
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import yaml from 'js-yaml'
|
|||||||
import fs from 'fs/promises';
|
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';
|
import { checkContainer } from './lib/docker';
|
||||||
import { migrateServicesToNewTemplate } from './lib';
|
import { migrateApplicationPersistentStorage, migrateServicesToNewTemplate } from './lib';
|
||||||
import { refreshTags, refreshTemplates } from './routes/api/v1/handlers';
|
import { refreshTags, refreshTemplates } from './routes/api/v1/handlers';
|
||||||
|
|
||||||
declare module 'fastify' {
|
declare module 'fastify' {
|
||||||
@@ -142,7 +142,8 @@ const host = '0.0.0.0';
|
|||||||
await socketIOServer(fastify)
|
await socketIOServer(fastify)
|
||||||
console.log(`Coolify's API is listening on ${host}:${port}`);
|
console.log(`Coolify's API is listening on ${host}:${port}`);
|
||||||
|
|
||||||
migrateServicesToNewTemplate()
|
migrateServicesToNewTemplate();
|
||||||
|
await migrateApplicationPersistentStorage();
|
||||||
await initServer();
|
await initServer();
|
||||||
|
|
||||||
const graceful = new Graceful({ brees: [scheduler] });
|
const graceful = new Graceful({ brees: [scheduler] });
|
||||||
@@ -181,7 +182,7 @@ const host = '0.0.0.0';
|
|||||||
|
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
await migrateServicesToNewTemplate()
|
await migrateServicesToNewTemplate()
|
||||||
}, 60000)
|
}, isDev ? 1000 : 60000)
|
||||||
|
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
await copySSLCertificates();
|
await copySSLCertificates();
|
||||||
|
|||||||
@@ -117,8 +117,10 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
let domain = getDomain(fqdn);
|
let domain = getDomain(fqdn);
|
||||||
const volumes =
|
const volumes =
|
||||||
persistentStorage?.map((storage) => {
|
persistentStorage?.map((storage) => {
|
||||||
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${buildPack !== 'docker' ? '/app' : ''
|
if (storage.oldPath) {
|
||||||
}${storage.path}`;
|
return `${applicationId}${storage.path.replace(/\//gi, '-').replace('-app','')}:${storage.path}`;
|
||||||
|
}
|
||||||
|
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
|
||||||
}) || [];
|
}) || [];
|
||||||
// Previews, we need to get the source branch and set subdomain
|
// Previews, we need to get the source branch and set subdomain
|
||||||
if (pullmergeRequestId) {
|
if (pullmergeRequestId) {
|
||||||
|
|||||||
@@ -1,7 +1,33 @@
|
|||||||
import cuid from "cuid";
|
import cuid from "cuid";
|
||||||
import { decrypt, encrypt, fixType, generatePassword, prisma } from "./lib/common";
|
import { decrypt, encrypt, fixType, generatePassword, generateToken, prisma } from "./lib/common";
|
||||||
import { getTemplates } from "./lib/services";
|
import { getTemplates } from "./lib/services";
|
||||||
|
|
||||||
|
export async function migrateApplicationPersistentStorage() {
|
||||||
|
const settings = await prisma.setting.findFirst()
|
||||||
|
if (settings) {
|
||||||
|
const { id: settingsId, applicationStoragePathMigrationFinished } = settings
|
||||||
|
try {
|
||||||
|
if (!applicationStoragePathMigrationFinished) {
|
||||||
|
const applications = await prisma.application.findMany({ include: { persistentStorage: true } });
|
||||||
|
for (const application of applications) {
|
||||||
|
if (application.persistentStorage && application.persistentStorage.length > 0 && application?.buildPack !== 'docker') {
|
||||||
|
for (const storage of application.persistentStorage) {
|
||||||
|
let { id, path } = storage
|
||||||
|
if (!path.startsWith('/app')) {
|
||||||
|
path = `/app${path}`
|
||||||
|
await prisma.applicationPersistentStorage.update({ where: { id }, data: { path, oldPath: true } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
} finally {
|
||||||
|
await prisma.setting.update({ where: { id: settingsId }, data: { applicationStoragePathMigrationFinished: true } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
export async function migrateServicesToNewTemplate() {
|
export async function migrateServicesToNewTemplate() {
|
||||||
// This function migrates old hardcoded services to the new template based services
|
// This function migrates old hardcoded services to the new template based services
|
||||||
try {
|
try {
|
||||||
@@ -57,39 +83,42 @@ export async function migrateServicesToNewTemplate() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
}
|
}
|
||||||
|
if (template.variables) {
|
||||||
if (template.variables.length > 0) {
|
if (template.variables.length > 0) {
|
||||||
|
for (const variable of template.variables) {
|
||||||
|
const { defaultValue } = variable;
|
||||||
|
const regex = /^\$\$.*\((\d+)\)$/g;
|
||||||
|
const length = Number(regex.exec(defaultValue)?.[1]) || undefined
|
||||||
|
if (variable.defaultValue.startsWith('$$generate_password')) {
|
||||||
|
variable.value = generatePassword({ length });
|
||||||
|
} else if (variable.defaultValue.startsWith('$$generate_hex')) {
|
||||||
|
variable.value = generatePassword({ length, isHex: true });
|
||||||
|
} else if (variable.defaultValue.startsWith('$$generate_username')) {
|
||||||
|
variable.value = cuid();
|
||||||
|
} else if (variable.defaultValue.startsWith('$$generate_token')) {
|
||||||
|
variable.value = generateToken()
|
||||||
|
} else {
|
||||||
|
variable.value = variable.defaultValue || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
for (const variable of template.variables) {
|
for (const variable of template.variables) {
|
||||||
const { defaultValue } = variable;
|
if (variable.id.startsWith('$$secret_')) {
|
||||||
const regex = /^\$\$.*\((\d+)\)$/g;
|
const found = await prisma.serviceSecret.findFirst({ where: { name: variable.name, serviceId: id } })
|
||||||
const length = Number(regex.exec(defaultValue)?.[1]) || undefined
|
if (!found) {
|
||||||
if (variable.defaultValue.startsWith('$$generate_password')) {
|
await prisma.serviceSecret.create({
|
||||||
variable.value = generatePassword({ length });
|
data: { name: variable.name, value: encrypt(variable.value) || '', service: { connect: { id } } }
|
||||||
} else if (variable.defaultValue.startsWith('$$generate_hex')) {
|
})
|
||||||
variable.value = generatePassword({ length, isHex: true });
|
}
|
||||||
} else if (variable.defaultValue.startsWith('$$generate_username')) {
|
|
||||||
variable.value = cuid();
|
|
||||||
} else {
|
|
||||||
variable.value = variable.defaultValue || '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const variable of template.variables) {
|
|
||||||
if (variable.id.startsWith('$$secret_')) {
|
|
||||||
const found = await prisma.serviceSecret.findFirst({ where: { name: variable.name, serviceId: id } })
|
|
||||||
if (!found) {
|
|
||||||
await prisma.serviceSecret.create({
|
|
||||||
data: { name: variable.name, value: encrypt(variable.value) || '', service: { connect: { id } } }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
if (variable.id.startsWith('$$config_')) {
|
if (variable.id.startsWith('$$config_')) {
|
||||||
const found = await prisma.serviceSetting.findFirst({ where: { name: variable.name, serviceId: id } })
|
const found = await prisma.serviceSetting.findFirst({ where: { name: variable.name, serviceId: id } })
|
||||||
if (!found) {
|
if (!found) {
|
||||||
await prisma.serviceSetting.create({
|
await prisma.serviceSetting.create({
|
||||||
data: { name: variable.name, value: variable.value.toString(), variableName: variable.id, service: { connect: { id } } }
|
data: { name: variable.name, value: variable.value.toString(), variableName: variable.id, service: { connect: { id } } }
|
||||||
})
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -458,7 +487,7 @@ async function migrateSettings(settings: any[], service: any, template: any) {
|
|||||||
// console.log('Migrating setting', name, value, 'for service', service.id, ', service name:', service.name, 'variableName: ', variableName)
|
// console.log('Migrating setting', name, value, 'for service', service.id, ', service name:', service.name, 'variableName: ', variableName)
|
||||||
|
|
||||||
await prisma.serviceSetting.findFirst({ where: { name: minio, serviceId: service.id } }) || await prisma.serviceSetting.create({ data: { name: minio, value, variableName, service: { connect: { id: service.id } } } })
|
await prisma.serviceSetting.findFirst({ where: { name: minio, serviceId: service.id } }) || await prisma.serviceSetting.create({ data: { name: minio, value, variableName, service: { connect: { id: service.id } } } })
|
||||||
} catch(error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -473,7 +502,7 @@ async function migrateSecrets(secrets: any[], service: any) {
|
|||||||
}
|
}
|
||||||
// console.log('Migrating secret', name, value, 'for service', service.id, ', service name:', service.name)
|
// console.log('Migrating secret', name, value, 'for service', service.id, ', service name:', service.name)
|
||||||
await prisma.serviceSecret.findFirst({ where: { name, serviceId: service.id } }) || await prisma.serviceSecret.create({ data: { name, value, service: { connect: { id: service.id } } } })
|
await prisma.serviceSecret.findFirst({ where: { name, serviceId: service.id } }) || await prisma.serviceSecret.create({ data: { name, value, service: { connect: { id: service.id } } } })
|
||||||
} catch(error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,9 +38,10 @@ export default async function (data) {
|
|||||||
if (!dockerComposeYaml.services) {
|
if (!dockerComposeYaml.services) {
|
||||||
throw 'No Services found in docker-compose file.'
|
throw 'No Services found in docker-compose file.'
|
||||||
}
|
}
|
||||||
const envs = [
|
const envs = [];
|
||||||
`PORT=${port}`
|
if (Object.entries(dockerComposeYaml.services).length === 1) {
|
||||||
];
|
envs.push(`PORT=${port}`)
|
||||||
|
}
|
||||||
if (secrets.length > 0) {
|
if (secrets.length > 0) {
|
||||||
secrets.forEach((secret) => {
|
secrets.forEach((secret) => {
|
||||||
if (pullmergeRequestId) {
|
if (pullmergeRequestId) {
|
||||||
@@ -64,19 +65,42 @@ export default async function (data) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
const composeVolumes = volumes.map((volume) => {
|
const composeVolumes = [];
|
||||||
return {
|
if (volumes.length > 0) {
|
||||||
[`${volume.split(':')[0]}`]: {
|
for (const volume of volumes) {
|
||||||
name: volume.split(':')[0]
|
let [v, path] = volume.split(':');
|
||||||
|
composeVolumes[v] = {
|
||||||
|
name: v,
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
let networks = {}
|
let networks = {}
|
||||||
for (let [key, value] of Object.entries(dockerComposeYaml.services)) {
|
for (let [key, value] of Object.entries(dockerComposeYaml.services)) {
|
||||||
value['container_name'] = `${applicationId}-${key}`
|
value['container_name'] = `${applicationId}-${key}`
|
||||||
value['env_file'] = envFound ? [`${workdir}/.env`] : []
|
value['env_file'] = envFound ? [`${workdir}/.env`] : []
|
||||||
value['labels'] = labels
|
value['labels'] = labels
|
||||||
value['volumes'] = volumes
|
// TODO: If we support separated volume for each service, we need to add it here
|
||||||
|
if (value['volumes']?.length > 0) {
|
||||||
|
value['volumes'] = value['volumes'].map((volume) => {
|
||||||
|
let [v, path, permission] = volume.split(':');
|
||||||
|
if (!path) {
|
||||||
|
path = v;
|
||||||
|
v = `${applicationId}${v.replace(/\//gi, '-')}`
|
||||||
|
} else {
|
||||||
|
v = `${applicationId}-${v}`
|
||||||
|
}
|
||||||
|
composeVolumes[v] = {
|
||||||
|
name: v
|
||||||
|
}
|
||||||
|
return `${v}:${path}${permission ? ':' + permission : ''}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (volumes.length > 0) {
|
||||||
|
for (const volume of volumes) {
|
||||||
|
value['volumes'].push(volume)
|
||||||
|
}
|
||||||
|
}
|
||||||
if (dockerComposeConfiguration[key].port) {
|
if (dockerComposeConfiguration[key].port) {
|
||||||
value['expose'] = [dockerComposeConfiguration[key].port]
|
value['expose'] = [dockerComposeConfiguration[key].port]
|
||||||
}
|
}
|
||||||
@@ -89,8 +113,11 @@ export default async function (data) {
|
|||||||
}
|
}
|
||||||
value['networks'] = [...value['networks'] || '', network]
|
value['networks'] = [...value['networks'] || '', network]
|
||||||
dockerComposeYaml.services[key] = { ...dockerComposeYaml.services[key], restart: defaultComposeConfiguration(network).restart, deploy: defaultComposeConfiguration(network).deploy }
|
dockerComposeYaml.services[key] = { ...dockerComposeYaml.services[key], restart: defaultComposeConfiguration(network).restart, deploy: defaultComposeConfiguration(network).deploy }
|
||||||
|
|
||||||
|
}
|
||||||
|
if (Object.keys(composeVolumes).length > 0) {
|
||||||
|
dockerComposeYaml['volumes'] = { ...composeVolumes }
|
||||||
}
|
}
|
||||||
dockerComposeYaml['volumes'] = Object.assign({ ...dockerComposeYaml['volumes'] }, ...composeVolumes)
|
|
||||||
dockerComposeYaml['networks'] = Object.assign({ ...networks }, { [network]: { external: true } })
|
dockerComposeYaml['networks'] = Object.assign({ ...networks }, { [network]: { external: true } })
|
||||||
await fs.writeFile(`${workdir}/docker-compose.${isYml ? 'yml' : 'yaml'}`, yaml.dump(dockerComposeYaml));
|
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 executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker compose --project-directory ${workdir} pull` })
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ import { promises as dns } from 'dns';
|
|||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import sshConfig from 'ssh-config';
|
import sshConfig from 'ssh-config';
|
||||||
|
import jsonwebtoken from 'jsonwebtoken';
|
||||||
import { checkContainer, removeContainer } from './docker';
|
import { checkContainer, removeContainer } from './docker';
|
||||||
import { day } from './dayjs';
|
import { day } from './dayjs';
|
||||||
import { saveBuildLog } from './buildPacks/common';
|
import { saveBuildLog } from './buildPacks/common';
|
||||||
import { scheduler } from './scheduler';
|
import { scheduler } from './scheduler';
|
||||||
|
|
||||||
export const version = '3.11.7';
|
export const version = '3.11.11';
|
||||||
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';
|
||||||
@@ -722,6 +722,11 @@ export async function listSettings(): Promise<any> {
|
|||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function generateToken() {
|
||||||
|
return jsonwebtoken.sign({
|
||||||
|
nbf: Math.floor(Date.now() / 1000) - 30,
|
||||||
|
}, process.env['COOLIFY_SECRET_KEY'])
|
||||||
|
}
|
||||||
export function generatePassword({
|
export function generatePassword({
|
||||||
length = 24,
|
length = 24,
|
||||||
symbols = false,
|
symbols = false,
|
||||||
@@ -1614,7 +1619,7 @@ export function persistentVolumes(id, persistentStorage, config) {
|
|||||||
for (const [key, value] of Object.entries(config)) {
|
for (const [key, value] of Object.entries(config)) {
|
||||||
if (value.volumes) {
|
if (value.volumes) {
|
||||||
for (const volume of value.volumes) {
|
for (const volume of value.volumes) {
|
||||||
if (!volume.startsWith('/var/run/docker.sock')) {
|
if (!volume.startsWith('/')) {
|
||||||
volumeSet.add(volume);
|
volumeSet.add(volume);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,9 +103,19 @@ export async function startService(request: FastifyRequest<ServiceStartStop>, fa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let port = null
|
let ports = []
|
||||||
if (template.services[s].ports?.length > 0) {
|
if (template.services[s].proxy?.length > 0) {
|
||||||
port = template.services[s].ports[0]
|
for (const proxy of template.services[s].proxy) {
|
||||||
|
if (proxy.hostPort) {
|
||||||
|
ports.push(`${proxy.hostPort}:${proxy.port}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (template.services[s].ports?.length === 1) {
|
||||||
|
for (const port of template.services[s].ports) {
|
||||||
|
ports.push(`${exposePort}:${exposePort}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let image = template.services[s].image
|
let image = template.services[s].image
|
||||||
if (arm && template.services[s].imageArm) {
|
if (arm && template.services[s].imageArm) {
|
||||||
@@ -118,7 +128,7 @@ export async function startService(request: FastifyRequest<ServiceStartStop>, fa
|
|||||||
entrypoint: template.services[s]?.entrypoint,
|
entrypoint: template.services[s]?.entrypoint,
|
||||||
image,
|
image,
|
||||||
expose: template.services[s].ports,
|
expose: template.services[s].ports,
|
||||||
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
|
ports,
|
||||||
volumes: Array.from(volumes),
|
volumes: Array.from(volumes),
|
||||||
environment: newEnvironments,
|
environment: newEnvironments,
|
||||||
depends_on: template.services[s]?.depends_on,
|
depends_on: template.services[s]?.depends_on,
|
||||||
@@ -128,7 +138,6 @@ export async function startService(request: FastifyRequest<ServiceStartStop>, fa
|
|||||||
labels: makeLabelForServices(type),
|
labels: makeLabelForServices(type),
|
||||||
...defaultComposeConfiguration(network),
|
...defaultComposeConfiguration(network),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate files for builds
|
// Generate files for builds
|
||||||
if (template.services[s]?.files?.length > 0) {
|
if (template.services[s]?.files?.length > 0) {
|
||||||
if (!config[s].build) {
|
if (!config[s].build) {
|
||||||
@@ -182,7 +191,6 @@ export async function startService(request: FastifyRequest<ServiceStartStop>, fa
|
|||||||
`docker container ls -a --filter 'name=${id}-' --format {{.ID}}|xargs -r -n 1 docker container rm -f`
|
`docker container ls -a --filter 'name=${id}-' --format {{.ID}}|xargs -r -n 1 docker container rm -f`
|
||||||
});
|
});
|
||||||
} catch (error) { }
|
} catch (error) { }
|
||||||
|
|
||||||
}
|
}
|
||||||
return {}
|
return {}
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import yaml from 'js-yaml';
|
|||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import cuid from 'cuid';
|
import cuid from 'cuid';
|
||||||
|
|
||||||
import { prisma, uniqueName, asyncExecShell, getServiceFromDB, getContainerUsage, isDomainConfigured, fixType, decrypt, encrypt, ComposeFile, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, executeDockerCmd, checkDomainsIsValidInDNS, checkExposedPort, listSettings } from '../../../../lib/common';
|
import { prisma, uniqueName, asyncExecShell, getServiceFromDB, getContainerUsage, isDomainConfigured, fixType, decrypt, encrypt, ComposeFile, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, executeDockerCmd, checkDomainsIsValidInDNS, checkExposedPort, listSettings, generateToken } from '../../../../lib/common';
|
||||||
import { day } from '../../../../lib/dayjs';
|
import { day } from '../../../../lib/dayjs';
|
||||||
import { checkContainer, } from '../../../../lib/docker';
|
import { checkContainer, } from '../../../../lib/docker';
|
||||||
import { removeService } from '../../../../lib/services/common';
|
import { removeService } from '../../../../lib/services/common';
|
||||||
@@ -82,14 +82,17 @@ export async function getServiceStatus(request: FastifyRequest<OnlyId>) {
|
|||||||
if (containersArray.length > 0 && containersArray[0] !== '') {
|
if (containersArray.length > 0 && containersArray[0] !== '') {
|
||||||
const templates = await getTemplates();
|
const templates = await getTemplates();
|
||||||
let template = templates.find(t => t.type === service.type);
|
let template = templates.find(t => t.type === service.type);
|
||||||
template = JSON.parse(JSON.stringify(template).replaceAll('$$id', service.id));
|
const templateStr = JSON.stringify(template)
|
||||||
|
if (templateStr) {
|
||||||
|
template = JSON.parse(templateStr.replaceAll('$$id', service.id));
|
||||||
|
}
|
||||||
for (const container of containersArray) {
|
for (const container of containersArray) {
|
||||||
let isRunning = false;
|
let isRunning = false;
|
||||||
let isExited = false;
|
let isExited = false;
|
||||||
let isRestarting = false;
|
let isRestarting = false;
|
||||||
let isExcluded = false;
|
let isExcluded = false;
|
||||||
const containerObj = JSON.parse(container);
|
const containerObj = JSON.parse(container);
|
||||||
const exclude = template.services[containerObj.Names]?.exclude;
|
const exclude = template?.services[containerObj.Names]?.exclude;
|
||||||
if (exclude) {
|
if (exclude) {
|
||||||
payload[containerObj.Names] = {
|
payload[containerObj.Names] = {
|
||||||
status: {
|
status: {
|
||||||
@@ -156,13 +159,17 @@ export async function parseAndFindServiceTemplates(service: any, workdir?: strin
|
|||||||
files: value?.files,
|
files: value?.files,
|
||||||
environment: [],
|
environment: [],
|
||||||
fqdns: [],
|
fqdns: [],
|
||||||
|
hostPorts: [],
|
||||||
proxy: {}
|
proxy: {}
|
||||||
}
|
}
|
||||||
if (value.environment?.length > 0) {
|
if (value.environment?.length > 0) {
|
||||||
for (const env of value.environment) {
|
for (const env of value.environment) {
|
||||||
let [envKey, ...envValue] = env.split('=')
|
let [envKey, ...envValue] = env.split('=')
|
||||||
envValue = envValue.join("=")
|
envValue = envValue.join("=")
|
||||||
const variable = foundTemplate.variables.find(v => v.name === envKey) || foundTemplate.variables.find(v => v.id === envValue)
|
let variable = null
|
||||||
|
if (foundTemplate?.variables) {
|
||||||
|
variable = foundTemplate?.variables.find(v => v.name === envKey) || foundTemplate?.variables.find(v => v.id === envValue)
|
||||||
|
}
|
||||||
if (variable) {
|
if (variable) {
|
||||||
const id = variable.id.replaceAll('$$', '')
|
const id = variable.id.replaceAll('$$', '')
|
||||||
const label = variable?.label
|
const label = variable?.label
|
||||||
@@ -188,7 +195,7 @@ export async function parseAndFindServiceTemplates(service: any, workdir?: strin
|
|||||||
if (value?.proxy && value.proxy.length > 0) {
|
if (value?.proxy && value.proxy.length > 0) {
|
||||||
for (const proxyValue of value.proxy) {
|
for (const proxyValue of value.proxy) {
|
||||||
if (proxyValue.domain) {
|
if (proxyValue.domain) {
|
||||||
const variable = foundTemplate.variables.find(v => v.id === proxyValue.domain)
|
const variable = foundTemplate?.variables.find(v => v.id === proxyValue.domain)
|
||||||
if (variable) {
|
if (variable) {
|
||||||
const { id, name, label, description, defaultValue, required = false } = variable
|
const { id, name, label, description, defaultValue, required = false } = variable
|
||||||
const found = await prisma.serviceSetting.findFirst({ where: { serviceId: service.id, variableName: proxyValue.domain } })
|
const found = await prisma.serviceSetting.findFirst({ where: { serviceId: service.id, variableName: proxyValue.domain } })
|
||||||
@@ -196,7 +203,16 @@ export async function parseAndFindServiceTemplates(service: any, workdir?: strin
|
|||||||
{ id, name, value: found?.value || '', label, description, defaultValue, required }
|
{ id, name, value: found?.value || '', label, description, defaultValue, required }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (proxyValue.hostPort) {
|
||||||
|
const variable = foundTemplate?.variables.find(v => v.id === proxyValue.hostPort)
|
||||||
|
if (variable) {
|
||||||
|
const { id, name, label, description, defaultValue, required = false } = variable
|
||||||
|
const found = await prisma.serviceSetting.findFirst({ where: { serviceId: service.id, variableName: proxyValue.hostPort } })
|
||||||
|
parsedTemplate[realKey].hostPorts.push(
|
||||||
|
{ id, name, value: found?.value || '', label, description, defaultValue, required }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,15 +235,17 @@ export async function parseAndFindServiceTemplates(service: any, workdir?: strin
|
|||||||
if (service.serviceSetting.length > 0) {
|
if (service.serviceSetting.length > 0) {
|
||||||
for (const setting of service.serviceSetting) {
|
for (const setting of service.serviceSetting) {
|
||||||
const { value, variableName } = setting
|
const { value, variableName } = setting
|
||||||
const regex = new RegExp(`\\$\\$config_${variableName.replace('$$config_', '')}`, 'gi')
|
const regex = new RegExp(`\\$\\$config_${variableName.replace('$$config_', '')}\"`, 'gi')
|
||||||
if (value === '$$generate_fqdn') {
|
if (value === '$$generate_fqdn') {
|
||||||
strParsedTemplate = strParsedTemplate.replaceAll(regex, service.fqdn || '')
|
strParsedTemplate = strParsedTemplate.replaceAll(regex, service.fqdn + '"' || '' + '"')
|
||||||
|
} else if (value === '$$generate_fqdn_slash') {
|
||||||
|
strParsedTemplate = strParsedTemplate.replaceAll(regex, service.fqdn + '/' + '"')
|
||||||
} else if (value === '$$generate_domain') {
|
} else if (value === '$$generate_domain') {
|
||||||
strParsedTemplate = strParsedTemplate.replaceAll(regex, getDomain(service.fqdn))
|
strParsedTemplate = strParsedTemplate.replaceAll(regex, getDomain(service.fqdn) + '"')
|
||||||
} else if (service.destinationDocker?.network && value === '$$generate_network') {
|
} else if (service.destinationDocker?.network && value === '$$generate_network') {
|
||||||
strParsedTemplate = strParsedTemplate.replaceAll(regex, service.destinationDocker.network)
|
strParsedTemplate = strParsedTemplate.replaceAll(regex, service.destinationDocker.network + '"')
|
||||||
} else {
|
} else {
|
||||||
strParsedTemplate = strParsedTemplate.replaceAll(regex, value)
|
strParsedTemplate = strParsedTemplate.replaceAll(regex, value + '"')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,11 +255,14 @@ export async function parseAndFindServiceTemplates(service: any, workdir?: strin
|
|||||||
for (const secret of service.serviceSecret) {
|
for (const secret of service.serviceSecret) {
|
||||||
let { name, value } = secret
|
let { name, value } = secret
|
||||||
name = name.toLowerCase()
|
name = name.toLowerCase()
|
||||||
const regexHashed = new RegExp(`\\$\\$hashed\\$\\$secret_${name}`, 'gi')
|
const regexHashed = new RegExp(`\\$\\$hashed\\$\\$secret_${name}\"`, 'gi')
|
||||||
const regex = new RegExp(`\\$\\$secret_${name}`, 'gi')
|
const regex = new RegExp(`\\$\\$secret_${name}\"`, 'gi')
|
||||||
if (value) {
|
if (value) {
|
||||||
strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, bcrypt.hashSync(value.replaceAll("\"", "\\\""), 10))
|
strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, bcrypt.hashSync(value.replaceAll("\"", "\\\""), 10) + '"')
|
||||||
strParsedTemplate = strParsedTemplate.replaceAll(regex, value.replaceAll("\"", "\\\""))
|
strParsedTemplate = strParsedTemplate.replaceAll(regex, value.replaceAll("\"", "\\\"") + '"')
|
||||||
|
} else {
|
||||||
|
strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, '' + '"')
|
||||||
|
strParsedTemplate = strParsedTemplate.replaceAll(regex, '' + '"')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -291,42 +312,46 @@ export async function saveServiceType(request: FastifyRequest<SaveServiceType>,
|
|||||||
let foundTemplate = templates.find(t => fixType(t.type) === fixType(type))
|
let foundTemplate = templates.find(t => fixType(t.type) === fixType(type))
|
||||||
if (foundTemplate) {
|
if (foundTemplate) {
|
||||||
foundTemplate = JSON.parse(JSON.stringify(foundTemplate).replaceAll('$$id', id))
|
foundTemplate = JSON.parse(JSON.stringify(foundTemplate).replaceAll('$$id', id))
|
||||||
if (foundTemplate.variables.length > 0) {
|
if (foundTemplate.variables) {
|
||||||
|
if (foundTemplate.variables.length > 0) {
|
||||||
|
for (const variable of foundTemplate.variables) {
|
||||||
|
const { defaultValue } = variable;
|
||||||
|
const regex = /^\$\$.*\((\d+)\)$/g;
|
||||||
|
const length = Number(regex.exec(defaultValue)?.[1]) || undefined
|
||||||
|
if (variable.defaultValue.startsWith('$$generate_password')) {
|
||||||
|
variable.value = generatePassword({ length });
|
||||||
|
} else if (variable.defaultValue.startsWith('$$generate_hex')) {
|
||||||
|
variable.value = generatePassword({ length, isHex: true });
|
||||||
|
} else if (variable.defaultValue.startsWith('$$generate_username')) {
|
||||||
|
variable.value = cuid();
|
||||||
|
} else if (variable.defaultValue.startsWith('$$generate_token')) {
|
||||||
|
variable.value = generateToken()
|
||||||
|
} else {
|
||||||
|
variable.value = variable.defaultValue || '';
|
||||||
|
}
|
||||||
|
const foundVariableSomewhereElse = foundTemplate.variables.find(v => v.defaultValue.includes(variable.id))
|
||||||
|
if (foundVariableSomewhereElse) {
|
||||||
|
foundVariableSomewhereElse.value = foundVariableSomewhereElse.value.replaceAll(variable.id, variable.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
for (const variable of foundTemplate.variables) {
|
for (const variable of foundTemplate.variables) {
|
||||||
const { defaultValue } = variable;
|
if (variable.id.startsWith('$$secret_')) {
|
||||||
const regex = /^\$\$.*\((\d+)\)$/g;
|
const found = await prisma.serviceSecret.findFirst({ where: { name: variable.name, serviceId: id } })
|
||||||
const length = Number(regex.exec(defaultValue)?.[1]) || undefined
|
if (!found) {
|
||||||
if (variable.defaultValue.startsWith('$$generate_password')) {
|
await prisma.serviceSecret.create({
|
||||||
variable.value = generatePassword({ length });
|
data: { name: variable.name, value: encrypt(variable.value) || '', service: { connect: { id } } }
|
||||||
} else if (variable.defaultValue.startsWith('$$generate_hex')) {
|
})
|
||||||
variable.value = generatePassword({ length, isHex: true });
|
}
|
||||||
} else if (variable.defaultValue.startsWith('$$generate_username')) {
|
|
||||||
variable.value = cuid();
|
|
||||||
} else {
|
|
||||||
variable.value = variable.defaultValue || '';
|
|
||||||
}
|
|
||||||
const foundVariableSomewhereElse = foundTemplate.variables.find(v => v.defaultValue.includes(variable.id))
|
|
||||||
if (foundVariableSomewhereElse) {
|
|
||||||
foundVariableSomewhereElse.value = foundVariableSomewhereElse.value.replaceAll(variable.id, variable.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const variable of foundTemplate.variables) {
|
|
||||||
if (variable.id.startsWith('$$secret_')) {
|
|
||||||
const found = await prisma.serviceSecret.findFirst({ where: { name: variable.name, serviceId: id } })
|
|
||||||
if (!found) {
|
|
||||||
await prisma.serviceSecret.create({
|
|
||||||
data: { name: variable.name, value: encrypt(variable.value) || '', service: { connect: { id } } }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
if (variable.id.startsWith('$$config_')) {
|
if (variable.id.startsWith('$$config_')) {
|
||||||
const found = await prisma.serviceSetting.findFirst({ where: { name: variable.name, serviceId: id } })
|
const found = await prisma.serviceSetting.findFirst({ where: { name: variable.name, serviceId: id } })
|
||||||
if (!found) {
|
if (!found) {
|
||||||
await prisma.serviceSetting.create({
|
await prisma.serviceSetting.create({
|
||||||
data: { name: variable.name, value: variable.value.toString(), variableName: variable.id, service: { connect: { id } } }
|
data: { name: variable.name, value: variable.value.toString(), variableName: variable.id, service: { connect: { id } } }
|
||||||
})
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -532,7 +557,7 @@ export async function saveService(request: FastifyRequest<SaveService>, reply: F
|
|||||||
}
|
}
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
if (!variableName) {
|
if (!variableName) {
|
||||||
variableName = foundTemplate.variables.find(v => v.name === name).id
|
variableName = foundTemplate?.variables.find(v => v.name === name).id
|
||||||
}
|
}
|
||||||
await prisma.serviceSetting.create({ data: { name, value, variableName, service: { connect: { id } } } })
|
await prisma.serviceSetting.create({ data: { name, value, variableName, service: { connect: { id } } } })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -395,8 +395,8 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
|
|||||||
}
|
}
|
||||||
found = JSON.parse(JSON.stringify(found).replaceAll('$$id', id));
|
found = JSON.parse(JSON.stringify(found).replaceAll('$$id', id));
|
||||||
for (const oneService of Object.keys(found.services)) {
|
for (const oneService of Object.keys(found.services)) {
|
||||||
const isProxyConfiguration = found.services[oneService].proxy;
|
const isDomainConfiguration = found?.services[oneService]?.proxy?.filter(p => p.domain) ?? [];
|
||||||
if (isProxyConfiguration) {
|
if (isDomainConfiguration.length > 0) {
|
||||||
const { proxy } = found.services[oneService];
|
const { proxy } = found.services[oneService];
|
||||||
for (let configuration of proxy) {
|
for (let configuration of proxy) {
|
||||||
if (configuration.domain) {
|
if (configuration.domain) {
|
||||||
@@ -431,20 +431,24 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (found.services[oneService].ports && found.services[oneService].ports.length > 0) {
|
if (found.services[oneService].ports && found.services[oneService].ports.length > 0) {
|
||||||
let port = found.services[oneService].ports[0]
|
for (let [index, port] of found.services[oneService].ports.entries()) {
|
||||||
const foundPortVariable = serviceSetting.find((a) => a.name.toLowerCase() === 'port')
|
if (port == 22) continue;
|
||||||
if (foundPortVariable) {
|
if (index === 0) {
|
||||||
port = foundPortVariable.value
|
const foundPortVariable = serviceSetting.find((a) => a.name.toLowerCase() === 'port')
|
||||||
|
if (foundPortVariable) {
|
||||||
|
port = foundPortVariable.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const domain = getDomain(fqdn);
|
||||||
|
const nakedDomain = domain.replace(/^www\./, '');
|
||||||
|
const isHttps = fqdn.startsWith('https://');
|
||||||
|
const isWWW = fqdn.includes('www.');
|
||||||
|
const pathPrefix = '/'
|
||||||
|
const isCustomSSL = false
|
||||||
|
const serviceId = `${oneService}-${port || 'default'}`
|
||||||
|
traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) }
|
||||||
|
traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, id, port) }
|
||||||
}
|
}
|
||||||
const domain = getDomain(fqdn);
|
|
||||||
const nakedDomain = domain.replace(/^www\./, '');
|
|
||||||
const isHttps = fqdn.startsWith('https://');
|
|
||||||
const isWWW = fqdn.includes('www.');
|
|
||||||
const pathPrefix = '/'
|
|
||||||
const isCustomSSL = false
|
|
||||||
const serviceId = `${oneService}-${port || 'default'}`
|
|
||||||
traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) }
|
|
||||||
traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, id, port) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -11,7 +11,7 @@ export function getAPIUrl() {
|
|||||||
return `https://${CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`;
|
return `https://${CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`;
|
||||||
}
|
}
|
||||||
return dev
|
return dev
|
||||||
? 'http://localhost:3001'
|
? `http://${window.location.hostname}:3001`
|
||||||
: 'http://localhost:3000';
|
: 'http://localhost:3000';
|
||||||
}
|
}
|
||||||
export function getWebhookUrl(type: string) {
|
export function getWebhookUrl(type: string) {
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let type: string;
|
export let type: string;
|
||||||
export let isAbsolute = false;
|
export let isAbsolute = false;
|
||||||
|
let fallback = '/icons/default.png';
|
||||||
|
const handleError = (ev: { target: { src: string } }) => (ev.target.src = fallback);
|
||||||
let extension = 'png';
|
let extension = 'png';
|
||||||
let svgs = [
|
let svgs = [
|
||||||
|
'gitea',
|
||||||
'languagetool',
|
'languagetool',
|
||||||
'meilisearch',
|
'meilisearch',
|
||||||
'n8n',
|
'n8n',
|
||||||
@@ -46,5 +49,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if name}
|
{#if name}
|
||||||
<img class={generateClass()} src={`/icons/${name}.${extension}`} alt={`Icon of ${name}`} />
|
<img
|
||||||
|
class={generateClass()}
|
||||||
|
src={`/icons/${name}.${extension}`}
|
||||||
|
on:error={handleError}
|
||||||
|
alt={`Icon of ${name}`}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -159,7 +159,7 @@
|
|||||||
"storage_saved": "Storage saved.",
|
"storage_saved": "Storage saved.",
|
||||||
"storage_updated": "Storage updated.",
|
"storage_updated": "Storage updated.",
|
||||||
"storage_deleted": "Storage deleted.",
|
"storage_deleted": "Storage deleted.",
|
||||||
"persistent_storage_explainer": "You can specify any folder that you want to be persistent across deployments.<br><span class='text-settings '>/example</span> means it will preserve <span class='text-settings '>/app/example</span> in the container as <span class='text-settings '>/app</span> is <span class='text-settings '>the root directory</span> for your application.<br><br>This is useful for storing data such as a <span class='text-settings '>database (SQLite)</span> or a <span class='text-settings '>cache</span>."
|
"persistent_storage_explainer": "You can specify any folder that you want to be persistent across deployments.<br><br><span class='text-settings '>/example</span> means it will preserve <span class='text-settings '>/example</span> between deployments.<br><br>Your application's data is copied to <span class='text-settings '>/app</span> inside the container, you can preserve data under it as well, like <span class='text-settings '>/app/db</span>.<br><br>This is useful for storing data such as a <span class='text-settings '>database (SQLite)</span> or a <span class='text-settings '>cache</span>."
|
||||||
},
|
},
|
||||||
"deployment_queued": "Deployment queued.",
|
"deployment_queued": "Deployment queued.",
|
||||||
"confirm_to_delete": "Are you sure you would like to delete '{{name}}'?",
|
"confirm_to_delete": "Are you sure you would like to delete '{{name}}'?",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import cuid from 'cuid';
|
|||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import { writable, readable, type Writable } from 'svelte/store';
|
import { writable, readable, type Writable } from 'svelte/store';
|
||||||
import { io as ioClient } from 'socket.io-client';
|
import { io as ioClient } from 'socket.io-client';
|
||||||
const socket = ioClient(dev ? 'http://localhost:3001' : '/', { auth: { token: Cookies.get('token') }, autoConnect: false });
|
const socket = ioClient(dev ? `http://${window.location.hostname}:3001` : '/', { auth: { token: Cookies.get('token') }, autoConnect: false });
|
||||||
|
|
||||||
export const io = socket;
|
export const io = socket;
|
||||||
interface AppSession {
|
interface AppSession {
|
||||||
|
|||||||
@@ -60,49 +60,54 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-full lg:px-0 px-4">
|
<div class="w-full lg:px-0 px-4">
|
||||||
<div class="grid grid-col-1 lg:grid-cols-3 lg:space-x-4" class:pt-8={isNew}>
|
{#if storage.predefined}
|
||||||
{#if storage.id}
|
<div class="flex flex-col lg:flex-row gap-4 pb-2">
|
||||||
<div class="flex flex-col">
|
<input disabled readonly class="w-full" value={storage.id} />
|
||||||
<label for="name" class="pb-2 uppercase font-bold">Volume name</label>
|
<input disabled readonly class="w-full" bind:value={storage.path} />
|
||||||
<input
|
</div>
|
||||||
disabled
|
{:else}
|
||||||
readonly
|
<div class="flex gap-4 pb-2" class:pt-8={isNew}>
|
||||||
class="w-full lg:w-64"
|
{#if storage.applicationId}
|
||||||
value="{storage.id}{storage.path.replace(/\//gi, '-')}"
|
{#if storage.oldPath}
|
||||||
/>
|
<input
|
||||||
</div>
|
disabled
|
||||||
{/if}
|
readonly
|
||||||
<div class="flex flex-col">
|
class="w-full"
|
||||||
<label for="name" class="pb-2 uppercase font-bold">{isNew ? 'New Path' : 'Path'}</label>
|
value="{storage.applicationId}{storage.path.replace(/\//gi, '-').replace('-app', '')}"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<input
|
||||||
|
disabled
|
||||||
|
readonly
|
||||||
|
class="w-full"
|
||||||
|
value="{storage.applicationId}{storage.path.replace(/\//gi, '-')}"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
<input
|
<input
|
||||||
class="w-full lg:w-64"
|
disabled={!isNew}
|
||||||
|
readonly={!isNew}
|
||||||
|
class="w-full"
|
||||||
bind:value={storage.path}
|
bind:value={storage.path}
|
||||||
required
|
required
|
||||||
placeholder="eg: /sqlite.db"
|
placeholder="eg: /data"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pt-8">
|
<div class="flex items-center justify-center">
|
||||||
{#if isNew}
|
{#if isNew}
|
||||||
<div class="flex items-center justify-center w-full lg:w-64">
|
<div class="w-full lg:w-64">
|
||||||
<button class="btn btn-sm btn-primary w-full" on:click={() => saveStorage(true)}
|
<button class="btn btn-sm btn-primary w-full" on:click={() => saveStorage(true)}
|
||||||
>{$t('forms.add')}</button
|
>{$t('forms.add')}</button
|
||||||
>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="flex flex-row items-center justify-center space-x-2 w-full lg:w-64">
|
|
||||||
<div class="flex items-center justify-center">
|
|
||||||
<button class="btn btn-sm btn-primary" on:click={() => saveStorage(false)}
|
|
||||||
>{$t('forms.set')}</button
|
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
{:else}
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<button class="btn btn-sm btn-error" on:click={removeStorage}
|
<button class="btn btn-sm btn-error" on:click={removeStorage}
|
||||||
>{$t('forms.remove')}</button
|
>{$t('forms.remove')}</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -501,7 +501,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="mx-auto max-w-screen-2xl px-0 lg:px-2 grid grid-cols-1"
|
class="mx-auto max-w-screen-2xl px-0 lg:px-10 grid grid-cols-1"
|
||||||
class:lg:grid-cols-4={!$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
|
class:lg:grid-cols-4={!$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
|
||||||
>
|
>
|
||||||
{#if !$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
|
{#if !$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
|
||||||
|
|||||||
@@ -61,20 +61,23 @@
|
|||||||
|
|
||||||
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
|
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
|
||||||
let statues: any = {};
|
let statues: any = {};
|
||||||
let loading = false;
|
let loading = {
|
||||||
|
save: false,
|
||||||
|
reloadCompose: false
|
||||||
|
};
|
||||||
let fqdnEl: any = null;
|
let fqdnEl: any = null;
|
||||||
let forceSave = false;
|
let forceSave = false;
|
||||||
let isPublicRepository = application.settings.isPublicRepository;
|
let isPublicRepository = application.settings?.isPublicRepository;
|
||||||
let apiUrl = application.gitSource.apiUrl;
|
let apiUrl = application.gitSource.apiUrl;
|
||||||
let branch = application.branch;
|
let branch = application.branch;
|
||||||
let repository = application.repository;
|
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;
|
||||||
let isCustomSSL = application.settings.isCustomSSL;
|
let isCustomSSL = application.settings?.isCustomSSL;
|
||||||
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 htmlUrl = application.gitSource.htmlUrl;
|
||||||
|
|
||||||
let dockerComposeFile = JSON.parse(application.dockerComposeFile) || null;
|
let dockerComposeFile = JSON.parse(application.dockerComposeFile) || null;
|
||||||
@@ -102,7 +105,6 @@
|
|||||||
label: 'Uvicorn'
|
label: 'Uvicorn'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
function normalizeDockerServices(services: any[]) {
|
function normalizeDockerServices(services: any[]) {
|
||||||
const tempdockerComposeServices = [];
|
const tempdockerComposeServices = [];
|
||||||
for (const [name, data] of Object.entries(services)) {
|
for (const [name, data] of Object.entries(services)) {
|
||||||
@@ -237,8 +239,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function handleSubmit(toast: boolean = true) {
|
async function handleSubmit(toast: boolean = true) {
|
||||||
if (loading) return;
|
if (loading.save) return;
|
||||||
if (toast) loading = true;
|
if (toast) loading.save = 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)
|
||||||
@@ -299,7 +301,7 @@
|
|||||||
}
|
}
|
||||||
return errorNotification(error);
|
return errorNotification(error);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading.save = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function selectWSGI(event: any) {
|
async function selectWSGI(event: any) {
|
||||||
@@ -361,6 +363,8 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
async function reloadCompose() {
|
async function reloadCompose() {
|
||||||
|
if (loading.reloadCompose) return;
|
||||||
|
loading.reloadCompose = true;
|
||||||
try {
|
try {
|
||||||
if (application.gitSource.type === 'github') {
|
if (application.gitSource.type === 'github') {
|
||||||
const headers = isPublicRepository
|
const headers = isPublicRepository
|
||||||
@@ -427,6 +431,8 @@
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorNotification(error);
|
errorNotification(error);
|
||||||
|
} finally {
|
||||||
|
loading.reloadCompose = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$: if ($status.application.statuses) {
|
$: if ($status.application.statuses) {
|
||||||
@@ -464,10 +470,10 @@
|
|||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-primary"
|
class="btn btn-sm btn-primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
class:loading
|
class:loading={loading.save}
|
||||||
class:bg-orange-600={forceSave}
|
class:bg-orange-600={forceSave}
|
||||||
class:hover:bg-orange-400={forceSave}
|
class:hover:bg-orange-400={forceSave}
|
||||||
disabled={loading}>{$t('forms.save')}</button
|
disabled={loading.save}>{$t('forms.save')}</button
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -569,13 +575,13 @@
|
|||||||
<input
|
<input
|
||||||
bind:this={fqdnEl}
|
bind:this={fqdnEl}
|
||||||
class="w-full"
|
class="w-full"
|
||||||
required={!application.settings.isBot}
|
required={!application.settings?.isBot}
|
||||||
readonly={isDisabled}
|
readonly={isDisabled}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
name="fqdn"
|
name="fqdn"
|
||||||
id="fqdn"
|
id="fqdn"
|
||||||
class:border={!application.settings.isBot && !application.fqdn}
|
class:border={!application.settings?.isBot && !application.fqdn}
|
||||||
class:border-red-500={!application.settings.isBot && !application.fqdn}
|
class:border-red-500={!application.settings?.isBot && !application.fqdn}
|
||||||
bind:value={application.fqdn}
|
bind:value={application.fqdn}
|
||||||
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
|
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
|
||||||
placeholder="eg: https://coollabs.io"
|
placeholder="eg: https://coollabs.io"
|
||||||
@@ -714,7 +720,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $features.beta}
|
{#if $features.beta}
|
||||||
{#if !application.settings.isBot && !application.settings.isPublicRepository}
|
{#if !application.settings?.isBot && !application.settings?.isPublicRepository}
|
||||||
<div class="grid grid-cols-2 items-center">
|
<div class="grid grid-cols-2 items-center">
|
||||||
<Setting
|
<Setting
|
||||||
id="isDBBranching"
|
id="isDBBranching"
|
||||||
@@ -993,8 +999,11 @@
|
|||||||
<div class="title font-bold pb-3 pt-10 border-b border-coolgray-500 mb-6">
|
<div class="title font-bold pb-3 pt-10 border-b border-coolgray-500 mb-6">
|
||||||
Stack <Beta />
|
Stack <Beta />
|
||||||
{#if $appSession.isAdmin}
|
{#if $appSession.isAdmin}
|
||||||
<button class="btn btn-sm btn-primary" on:click|preventDefault={reloadCompose}
|
<button
|
||||||
>Reload Docker Compose File</button
|
class="btn btn-sm btn-primary"
|
||||||
|
class:loading={loading.reloadCompose}
|
||||||
|
disabled={loading.reloadCompose}
|
||||||
|
on:click|preventDefault={reloadCompose}>Reload Docker Compose File</button
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
import type { Load } from '@sveltejs/kit';
|
import type { Load } from '@sveltejs/kit';
|
||||||
export const load: Load = async ({ params, stuff, url }) => {
|
export const load: Load = async ({ params, stuff, url }) => {
|
||||||
try {
|
try {
|
||||||
|
const { application } = stuff;
|
||||||
const response = await get(`/applications/${params.id}/storages`);
|
const response = await get(`/applications/${params.id}/storages`);
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
application,
|
||||||
...response
|
...response
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -19,12 +21,31 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let persistentStorages: any;
|
export let persistentStorages: any;
|
||||||
|
export let application: any;
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import Storage from './_Storage.svelte';
|
import Storage from './_Storage.svelte';
|
||||||
import { get } from '$lib/api';
|
import { get } from '$lib/api';
|
||||||
import { t } from '$lib/translations';
|
import { t } from '$lib/translations';
|
||||||
import Explainer from '$lib/components/Explainer.svelte';
|
import Explainer from '$lib/components/Explainer.svelte';
|
||||||
|
|
||||||
|
let composeJson = JSON.parse(application?.dockerComposeFile || '{}');
|
||||||
|
let predefinedVolumes: any[] = [];
|
||||||
|
if (composeJson?.services) {
|
||||||
|
for (const [_, service] of Object.entries(composeJson.services)) {
|
||||||
|
if (service?.volumes) {
|
||||||
|
for (const [_, volumeName] of Object.entries(service.volumes)) {
|
||||||
|
let [volume, target] = volumeName.split(':');
|
||||||
|
if (!target) {
|
||||||
|
target = volume;
|
||||||
|
volume = `${application.id}${volume.replace(/\//gi, '-')}`;
|
||||||
|
} else {
|
||||||
|
volume = `${application.id}-${volume}`;
|
||||||
|
}
|
||||||
|
predefinedVolumes.push({ id: volume, path: target, predefined: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const { id } = $page.params;
|
const { id } = $page.params;
|
||||||
async function refreshStorage() {
|
async function refreshStorage() {
|
||||||
const data = await get(`/applications/${id}/storages`);
|
const data = await get(`/applications/${id}/storages`);
|
||||||
@@ -34,20 +55,40 @@
|
|||||||
|
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<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">
|
<div class="title font-bold pb-3">Persistent Volumes</div>
|
||||||
Persistent Volumes <Explainer
|
|
||||||
position="dropdown-bottom"
|
|
||||||
explanation={$t('application.storage.persistent_storage_explainer')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
{#if predefinedVolumes.length > 0}
|
||||||
|
<div class="title">Predefined Volumes</div>
|
||||||
|
<div class="w-full lg:px-0 px-4">
|
||||||
|
<div class="grid grid-col-1 lg:grid-cols-2 py-2 gap-2">
|
||||||
|
<div class="font-bold uppercase">Volume Id</div>
|
||||||
|
<div class="font-bold uppercase">Mount Dir</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gap-4">
|
||||||
|
{#each predefinedVolumes as storage}
|
||||||
|
{#key storage.id}
|
||||||
|
<Storage on:refresh={refreshStorage} {storage} />
|
||||||
|
{/key}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if persistentStorages.length > 0}
|
||||||
|
<div class="title" class:pt-10={predefinedVolumes.length > 0}>Custom Volumes</div>
|
||||||
|
{/if}
|
||||||
{#each persistentStorages as storage}
|
{#each persistentStorages as storage}
|
||||||
{#key storage.id}
|
{#key storage.id}
|
||||||
<Storage on:refresh={refreshStorage} {storage} />
|
<Storage on:refresh={refreshStorage} {storage} />
|
||||||
{/key}
|
{/key}
|
||||||
{/each}
|
{/each}
|
||||||
|
<div class="title pt-10">
|
||||||
|
Add New Volume <Explainer
|
||||||
|
position="dropdown-bottom"
|
||||||
|
explanation={$t('application.storage.persistent_storage_explainer')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<Storage on:refresh={refreshStorage} isNew />
|
<Storage on:refresh={refreshStorage} isNew />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,6 +38,11 @@
|
|||||||
import Wordpress from './_Services/wordpress.svelte';
|
import Wordpress from './_Services/wordpress.svelte';
|
||||||
|
|
||||||
const { id } = $page.params;
|
const { id } = $page.params;
|
||||||
|
let hostPorts = Object.keys(template).filter((key) => {
|
||||||
|
if (template[key]?.hostPorts?.length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
$: isDisabled =
|
$: isDisabled =
|
||||||
!$appSession.isAdmin ||
|
!$appSession.isAdmin ||
|
||||||
$status.service.overallStatus === 'degraded' ||
|
$status.service.overallStatus === 'degraded' ||
|
||||||
@@ -291,7 +296,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<input class="w-full border-red-500" disabled placeholder="Error getting tags...">
|
<input class="w-full border-red-500" disabled placeholder="Error getting tags..." />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -389,22 +394,24 @@
|
|||||||
on:click={() => !$status.service.isRunning && changeSettings('dualCerts')}
|
on:click={() => !$status.service.isRunning && changeSettings('dualCerts')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 items-center">
|
{#if hostPorts.length === 0}
|
||||||
<label for="exposePort"
|
<div class="grid grid-cols-2 items-center">
|
||||||
>Exposed Port <Explainer
|
<label for="exposePort"
|
||||||
explanation={'You can expose your application to a port on the host system.<br><br>Useful if you would like to use your own reverse proxy or tunnel and also in development mode. Otherwise leave empty.'}
|
>Exposed Port <Explainer
|
||||||
/></label
|
explanation={'You can expose your application to a port on the host system.<br><br>Useful if you would like to use your own reverse proxy or tunnel and also in development mode. Otherwise leave empty.'}
|
||||||
>
|
/></label
|
||||||
<input
|
>
|
||||||
class="w-full"
|
<input
|
||||||
readonly={isDisabled}
|
class="w-full"
|
||||||
disabled={isDisabled}
|
readonly={isDisabled}
|
||||||
name="exposePort"
|
disabled={isDisabled}
|
||||||
id="exposePort"
|
name="exposePort"
|
||||||
bind:value={service.exposePort}
|
id="exposePort"
|
||||||
placeholder="12345"
|
bind:value={service.exposePort}
|
||||||
/>
|
placeholder="12345"
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="pt-6">
|
<div class="pt-6">
|
||||||
{#each Object.keys(template) as oneService}
|
{#each Object.keys(template) as oneService}
|
||||||
@@ -444,6 +451,16 @@
|
|||||||
placeholder={variable.placeholder}
|
placeholder={variable.placeholder}
|
||||||
required={variable?.required}
|
required={variable?.required}
|
||||||
/>
|
/>
|
||||||
|
{:else if variable.defaultValue === '$$generate_fqdn_slash'}
|
||||||
|
<CopyPasswordField
|
||||||
|
disabled
|
||||||
|
readonly
|
||||||
|
name={variable.name}
|
||||||
|
id={variable.name}
|
||||||
|
value={service.fqdn + '/' || ''}
|
||||||
|
placeholder={variable.placeholder}
|
||||||
|
required={variable?.required}
|
||||||
|
/>
|
||||||
{:else if variable.defaultValue === '$$generate_domain'}
|
{:else if variable.defaultValue === '$$generate_domain'}
|
||||||
<CopyPasswordField
|
<CopyPasswordField
|
||||||
disabled
|
disabled
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
|
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<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">
|
<div class="title font-bold pb-3">
|
||||||
Persistent Volumes <Explainer
|
Persistent Volumes <Explainer
|
||||||
position="dropdown-bottom"
|
position="dropdown-bottom"
|
||||||
|
|||||||
BIN
apps/ui/static/icons/default.png
Normal file
BIN
apps/ui/static/icons/default.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 141 KiB |
1
apps/ui/static/icons/gitea.svg
Normal file
1
apps/ui/static/icons/gitea.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" style="enable-background:new 0 0 640 640" xml:space="preserve" width="32" height="32"><path style="fill:#fff" d="m395.9 484.2-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12z"/><path style="fill:#609926" d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6zM125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1zm300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1z"/><path style="fill:#609926" d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8-1.9 8 2 16.3 9.1 20 7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3 7.8 4 17.4 1.7 22.5-5.3 5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8l-24.6 50.4z"/></svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
BIN
apps/ui/static/icons/keycloak.png
Normal file
BIN
apps/ui/static/icons/keycloak.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "coolify",
|
"name": "coolify",
|
||||||
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
|
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
|
||||||
"version": "3.11.7",
|
"version": "3.11.11",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"repository": "github:coollabsio/coolify",
|
"repository": "github:coollabsio/coolify",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Reference in New Issue
Block a user