mirror of
https://github.com/ershisan99/coolify.git
synced 2025-12-31 12:34:03 +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:
|
||||
push:
|
||||
paths:
|
||||
- '**'
|
||||
- "!others/fluentbit"
|
||||
- "!.github/workflows/fluent-bit-release.yml"
|
||||
branches:
|
||||
- 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
|
||||
defaultVersion: v3.6
|
||||
documentation: https://github.com/freyacodes/Lavalink
|
||||
@@ -53,7 +206,7 @@
|
||||
- id: $$config_port
|
||||
name: PORT
|
||||
label: Port
|
||||
defaultValue: '2333'
|
||||
defaultValue: "2333"
|
||||
required: true
|
||||
- id: $$secret_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 {
|
||||
id String @id @default(cuid())
|
||||
fqdn String? @unique
|
||||
isAPIDebuggingEnabled Boolean? @default(false)
|
||||
isRegistrationEnabled Boolean @default(false)
|
||||
dualCerts Boolean @default(false)
|
||||
minPort Int @default(9000)
|
||||
maxPort Int @default(9100)
|
||||
proxyPassword String
|
||||
proxyUser String
|
||||
proxyHash String?
|
||||
proxyDefaultRedirect String?
|
||||
isAutoUpdateEnabled Boolean @default(false)
|
||||
isDNSCheckEnabled Boolean @default(true)
|
||||
DNSServers String?
|
||||
isTraefikUsed Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
ipv4 String?
|
||||
ipv6 String?
|
||||
arch String?
|
||||
concurrentBuilds Int @default(1)
|
||||
id String @id @default(cuid())
|
||||
fqdn String? @unique
|
||||
isAPIDebuggingEnabled Boolean? @default(false)
|
||||
isRegistrationEnabled Boolean @default(false)
|
||||
dualCerts Boolean @default(false)
|
||||
minPort Int @default(9000)
|
||||
maxPort Int @default(9100)
|
||||
proxyPassword String
|
||||
proxyUser String
|
||||
proxyHash String?
|
||||
proxyDefaultRedirect String?
|
||||
isAutoUpdateEnabled Boolean @default(false)
|
||||
isDNSCheckEnabled Boolean @default(true)
|
||||
DNSServers String?
|
||||
isTraefikUsed Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
ipv4 String?
|
||||
ipv6 String?
|
||||
arch String?
|
||||
concurrentBuilds Int @default(1)
|
||||
applicationStoragePathMigrationFinished Boolean @default(false)
|
||||
}
|
||||
|
||||
model User {
|
||||
@@ -186,6 +187,7 @@ model ApplicationPersistentStorage {
|
||||
id String @id @default(cuid())
|
||||
applicationId String
|
||||
path String
|
||||
oldPath Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
application Application @relation(fields: [applicationId], references: [id])
|
||||
|
||||
@@ -17,7 +17,7 @@ import yaml from 'js-yaml'
|
||||
import fs from 'fs/promises';
|
||||
import { verifyRemoteDockerEngineFn } from './routes/api/v1/destinations/handlers';
|
||||
import { checkContainer } from './lib/docker';
|
||||
import { migrateServicesToNewTemplate } from './lib';
|
||||
import { migrateApplicationPersistentStorage, migrateServicesToNewTemplate } from './lib';
|
||||
import { refreshTags, refreshTemplates } from './routes/api/v1/handlers';
|
||||
|
||||
declare module 'fastify' {
|
||||
@@ -142,7 +142,8 @@ const host = '0.0.0.0';
|
||||
await socketIOServer(fastify)
|
||||
console.log(`Coolify's API is listening on ${host}:${port}`);
|
||||
|
||||
migrateServicesToNewTemplate()
|
||||
migrateServicesToNewTemplate();
|
||||
await migrateApplicationPersistentStorage();
|
||||
await initServer();
|
||||
|
||||
const graceful = new Graceful({ brees: [scheduler] });
|
||||
@@ -181,7 +182,7 @@ const host = '0.0.0.0';
|
||||
|
||||
setInterval(async () => {
|
||||
await migrateServicesToNewTemplate()
|
||||
}, 60000)
|
||||
}, isDev ? 1000 : 60000)
|
||||
|
||||
setInterval(async () => {
|
||||
await copySSLCertificates();
|
||||
|
||||
@@ -117,8 +117,10 @@ import * as buildpacks from '../lib/buildPacks';
|
||||
let domain = getDomain(fqdn);
|
||||
const volumes =
|
||||
persistentStorage?.map((storage) => {
|
||||
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${buildPack !== 'docker' ? '/app' : ''
|
||||
}${storage.path}`;
|
||||
if (storage.oldPath) {
|
||||
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
|
||||
if (pullmergeRequestId) {
|
||||
|
||||
@@ -1,7 +1,33 @@
|
||||
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";
|
||||
|
||||
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() {
|
||||
// This function migrates old hardcoded services to the new template based services
|
||||
try {
|
||||
@@ -57,39 +83,42 @@ export async function migrateServicesToNewTemplate() {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
|
||||
if (template.variables.length > 0) {
|
||||
if (template.variables) {
|
||||
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) {
|
||||
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 {
|
||||
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('$$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_')) {
|
||||
const found = await prisma.serviceSetting.findFirst({ where: { name: variable.name, serviceId: id } })
|
||||
if (!found) {
|
||||
await prisma.serviceSetting.create({
|
||||
data: { name: variable.name, value: variable.value.toString(), variableName: variable.id, service: { connect: { id } } }
|
||||
})
|
||||
}
|
||||
if (variable.id.startsWith('$$config_')) {
|
||||
const found = await prisma.serviceSetting.findFirst({ where: { name: variable.name, serviceId: id } })
|
||||
if (!found) {
|
||||
await prisma.serviceSetting.create({
|
||||
data: { name: variable.name, value: variable.value.toString(), variableName: variable.id, service: { connect: { id } } }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -456,9 +485,9 @@ async function migrateSettings(settings: any[], service: any, template: any) {
|
||||
variableName = `$$config_${name.toLowerCase()}`
|
||||
}
|
||||
// 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 } } } })
|
||||
} catch(error) {
|
||||
} catch (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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,9 +38,10 @@ export default async function (data) {
|
||||
if (!dockerComposeYaml.services) {
|
||||
throw 'No Services found in docker-compose file.'
|
||||
}
|
||||
const envs = [
|
||||
`PORT=${port}`
|
||||
];
|
||||
const envs = [];
|
||||
if (Object.entries(dockerComposeYaml.services).length === 1) {
|
||||
envs.push(`PORT=${port}`)
|
||||
}
|
||||
if (secrets.length > 0) {
|
||||
secrets.forEach((secret) => {
|
||||
if (pullmergeRequestId) {
|
||||
@@ -64,19 +65,42 @@ export default async function (data) {
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
const composeVolumes = volumes.map((volume) => {
|
||||
return {
|
||||
[`${volume.split(':')[0]}`]: {
|
||||
name: volume.split(':')[0]
|
||||
const composeVolumes = [];
|
||||
if (volumes.length > 0) {
|
||||
for (const volume of volumes) {
|
||||
let [v, path] = volume.split(':');
|
||||
composeVolumes[v] = {
|
||||
name: v,
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// 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) {
|
||||
value['expose'] = [dockerComposeConfiguration[key].port]
|
||||
}
|
||||
@@ -89,8 +113,11 @@ export default async function (data) {
|
||||
}
|
||||
value['networks'] = [...value['networks'] || '', network]
|
||||
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 } })
|
||||
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` })
|
||||
|
||||
@@ -11,13 +11,13 @@ import { promises as dns } from 'dns';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import os from 'os';
|
||||
import sshConfig from 'ssh-config';
|
||||
|
||||
import jsonwebtoken from 'jsonwebtoken';
|
||||
import { checkContainer, removeContainer } from './docker';
|
||||
import { day } from './dayjs';
|
||||
import { saveBuildLog } from './buildPacks/common';
|
||||
import { scheduler } from './scheduler';
|
||||
|
||||
export const version = '3.11.7';
|
||||
export const version = '3.11.11';
|
||||
export const isDev = process.env.NODE_ENV === 'development';
|
||||
|
||||
const algorithm = 'aes-256-ctr';
|
||||
@@ -722,6 +722,11 @@ export async function listSettings(): Promise<any> {
|
||||
return settings;
|
||||
}
|
||||
|
||||
export function generateToken() {
|
||||
return jsonwebtoken.sign({
|
||||
nbf: Math.floor(Date.now() / 1000) - 30,
|
||||
}, process.env['COOLIFY_SECRET_KEY'])
|
||||
}
|
||||
export function generatePassword({
|
||||
length = 24,
|
||||
symbols = false,
|
||||
@@ -1614,7 +1619,7 @@ export function persistentVolumes(id, persistentStorage, config) {
|
||||
for (const [key, value] of Object.entries(config)) {
|
||||
if (value.volumes) {
|
||||
for (const volume of value.volumes) {
|
||||
if (!volume.startsWith('/var/run/docker.sock')) {
|
||||
if (!volume.startsWith('/')) {
|
||||
volumeSet.add(volume);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,9 +103,19 @@ export async function startService(request: FastifyRequest<ServiceStartStop>, fa
|
||||
}
|
||||
}
|
||||
}
|
||||
let port = null
|
||||
if (template.services[s].ports?.length > 0) {
|
||||
port = template.services[s].ports[0]
|
||||
let ports = []
|
||||
if (template.services[s].proxy?.length > 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
|
||||
if (arm && template.services[s].imageArm) {
|
||||
@@ -118,7 +128,7 @@ export async function startService(request: FastifyRequest<ServiceStartStop>, fa
|
||||
entrypoint: template.services[s]?.entrypoint,
|
||||
image,
|
||||
expose: template.services[s].ports,
|
||||
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
|
||||
ports,
|
||||
volumes: Array.from(volumes),
|
||||
environment: newEnvironments,
|
||||
depends_on: template.services[s]?.depends_on,
|
||||
@@ -128,7 +138,6 @@ export async function startService(request: FastifyRequest<ServiceStartStop>, fa
|
||||
labels: makeLabelForServices(type),
|
||||
...defaultComposeConfiguration(network),
|
||||
}
|
||||
|
||||
// Generate files for builds
|
||||
if (template.services[s]?.files?.length > 0) {
|
||||
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`
|
||||
});
|
||||
} catch (error) { }
|
||||
|
||||
}
|
||||
return {}
|
||||
} catch ({ status, message }) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import yaml from 'js-yaml';
|
||||
import bcrypt from 'bcryptjs';
|
||||
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 { checkContainer, } from '../../../../lib/docker';
|
||||
import { removeService } from '../../../../lib/services/common';
|
||||
@@ -82,14 +82,17 @@ export async function getServiceStatus(request: FastifyRequest<OnlyId>) {
|
||||
if (containersArray.length > 0 && containersArray[0] !== '') {
|
||||
const templates = await getTemplates();
|
||||
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) {
|
||||
let isRunning = false;
|
||||
let isExited = false;
|
||||
let isRestarting = false;
|
||||
let isExcluded = false;
|
||||
const containerObj = JSON.parse(container);
|
||||
const exclude = template.services[containerObj.Names]?.exclude;
|
||||
const exclude = template?.services[containerObj.Names]?.exclude;
|
||||
if (exclude) {
|
||||
payload[containerObj.Names] = {
|
||||
status: {
|
||||
@@ -156,13 +159,17 @@ export async function parseAndFindServiceTemplates(service: any, workdir?: strin
|
||||
files: value?.files,
|
||||
environment: [],
|
||||
fqdns: [],
|
||||
hostPorts: [],
|
||||
proxy: {}
|
||||
}
|
||||
if (value.environment?.length > 0) {
|
||||
for (const env of value.environment) {
|
||||
let [envKey, ...envValue] = env.split('=')
|
||||
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) {
|
||||
const id = variable.id.replaceAll('$$', '')
|
||||
const label = variable?.label
|
||||
@@ -188,7 +195,7 @@ export async function parseAndFindServiceTemplates(service: any, workdir?: strin
|
||||
if (value?.proxy && value.proxy.length > 0) {
|
||||
for (const proxyValue of value.proxy) {
|
||||
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) {
|
||||
const { id, name, label, description, defaultValue, required = false } = variable
|
||||
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 }
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
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) {
|
||||
for (const setting of service.serviceSetting) {
|
||||
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') {
|
||||
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') {
|
||||
strParsedTemplate = strParsedTemplate.replaceAll(regex, getDomain(service.fqdn))
|
||||
strParsedTemplate = strParsedTemplate.replaceAll(regex, getDomain(service.fqdn) + '"')
|
||||
} else if (service.destinationDocker?.network && value === '$$generate_network') {
|
||||
strParsedTemplate = strParsedTemplate.replaceAll(regex, service.destinationDocker.network)
|
||||
strParsedTemplate = strParsedTemplate.replaceAll(regex, service.destinationDocker.network + '"')
|
||||
} 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) {
|
||||
let { name, value } = secret
|
||||
name = name.toLowerCase()
|
||||
const regexHashed = new RegExp(`\\$\\$hashed\\$\\$secret_${name}`, 'gi')
|
||||
const regex = new RegExp(`\\$\\$secret_${name}`, 'gi')
|
||||
const regexHashed = new RegExp(`\\$\\$hashed\\$\\$secret_${name}\"`, 'gi')
|
||||
const regex = new RegExp(`\\$\\$secret_${name}\"`, 'gi')
|
||||
if (value) {
|
||||
strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, bcrypt.hashSync(value.replaceAll("\"", "\\\""), 10))
|
||||
strParsedTemplate = strParsedTemplate.replaceAll(regex, value.replaceAll("\"", "\\\""))
|
||||
strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, bcrypt.hashSync(value.replaceAll("\"", "\\\""), 10) + '"')
|
||||
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))
|
||||
if (foundTemplate) {
|
||||
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) {
|
||||
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 {
|
||||
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('$$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_')) {
|
||||
const found = await prisma.serviceSetting.findFirst({ where: { name: variable.name, serviceId: id } })
|
||||
if (!found) {
|
||||
await prisma.serviceSetting.create({
|
||||
data: { name: variable.name, value: variable.value.toString(), variableName: variable.id, service: { connect: { id } } }
|
||||
})
|
||||
}
|
||||
if (variable.id.startsWith('$$config_')) {
|
||||
const found = await prisma.serviceSetting.findFirst({ where: { name: variable.name, serviceId: id } })
|
||||
if (!found) {
|
||||
await prisma.serviceSetting.create({
|
||||
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 (!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 } } } })
|
||||
}
|
||||
|
||||
@@ -395,8 +395,8 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
|
||||
}
|
||||
found = JSON.parse(JSON.stringify(found).replaceAll('$$id', id));
|
||||
for (const oneService of Object.keys(found.services)) {
|
||||
const isProxyConfiguration = found.services[oneService].proxy;
|
||||
if (isProxyConfiguration) {
|
||||
const isDomainConfiguration = found?.services[oneService]?.proxy?.filter(p => p.domain) ?? [];
|
||||
if (isDomainConfiguration.length > 0) {
|
||||
const { proxy } = found.services[oneService];
|
||||
for (let configuration of proxy) {
|
||||
if (configuration.domain) {
|
||||
@@ -431,20 +431,24 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
|
||||
}
|
||||
} else {
|
||||
if (found.services[oneService].ports && found.services[oneService].ports.length > 0) {
|
||||
let port = found.services[oneService].ports[0]
|
||||
const foundPortVariable = serviceSetting.find((a) => a.name.toLowerCase() === 'port')
|
||||
if (foundPortVariable) {
|
||||
port = foundPortVariable.value
|
||||
for (let [index, port] of found.services[oneService].ports.entries()) {
|
||||
if (port == 22) continue;
|
||||
if (index === 0) {
|
||||
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 dev
|
||||
? 'http://localhost:3001'
|
||||
? `http://${window.location.hostname}:3001`
|
||||
: 'http://localhost:3000';
|
||||
}
|
||||
export function getWebhookUrl(type: string) {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<script lang="ts">
|
||||
export let type: string;
|
||||
export let isAbsolute = false;
|
||||
let fallback = '/icons/default.png';
|
||||
const handleError = (ev: { target: { src: string } }) => (ev.target.src = fallback);
|
||||
let extension = 'png';
|
||||
let svgs = [
|
||||
'gitea',
|
||||
'languagetool',
|
||||
'meilisearch',
|
||||
'n8n',
|
||||
@@ -46,5 +49,10 @@
|
||||
</script>
|
||||
|
||||
{#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}
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
"storage_saved": "Storage saved.",
|
||||
"storage_updated": "Storage updated.",
|
||||
"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.",
|
||||
"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 { writable, readable, type Writable } from 'svelte/store';
|
||||
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;
|
||||
interface AppSession {
|
||||
|
||||
@@ -60,49 +60,54 @@
|
||||
</script>
|
||||
|
||||
<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.id}
|
||||
<div class="flex flex-col">
|
||||
<label for="name" class="pb-2 uppercase font-bold">Volume name</label>
|
||||
<input
|
||||
disabled
|
||||
readonly
|
||||
class="w-full lg:w-64"
|
||||
value="{storage.id}{storage.path.replace(/\//gi, '-')}"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-col">
|
||||
<label for="name" class="pb-2 uppercase font-bold">{isNew ? 'New Path' : 'Path'}</label>
|
||||
{#if storage.predefined}
|
||||
<div class="flex flex-col lg:flex-row gap-4 pb-2">
|
||||
<input disabled readonly class="w-full" value={storage.id} />
|
||||
<input disabled readonly class="w-full" bind:value={storage.path} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex gap-4 pb-2" class:pt-8={isNew}>
|
||||
{#if storage.applicationId}
|
||||
{#if storage.oldPath}
|
||||
<input
|
||||
disabled
|
||||
readonly
|
||||
class="w-full"
|
||||
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
|
||||
class="w-full lg:w-64"
|
||||
disabled={!isNew}
|
||||
readonly={!isNew}
|
||||
class="w-full"
|
||||
bind:value={storage.path}
|
||||
required
|
||||
placeholder="eg: /sqlite.db"
|
||||
placeholder="eg: /data"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pt-8">
|
||||
{#if isNew}
|
||||
<div class="flex items-center justify-center w-full lg:w-64">
|
||||
<button class="btn btn-sm btn-primary w-full" on:click={() => saveStorage(true)}
|
||||
>{$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 class="flex items-center justify-center">
|
||||
{#if isNew}
|
||||
<div class="w-full lg:w-64">
|
||||
<button class="btn btn-sm btn-primary w-full" on:click={() => saveStorage(true)}
|
||||
>{$t('forms.add')}</button
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex justify-center">
|
||||
<button class="btn btn-sm btn-error" on:click={removeStorage}
|
||||
>{$t('forms.remove')}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -501,7 +501,7 @@
|
||||
</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/`)}
|
||||
>
|
||||
{#if !$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
|
||||
|
||||
@@ -61,20 +61,23 @@
|
||||
|
||||
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
|
||||
let statues: any = {};
|
||||
let loading = false;
|
||||
let loading = {
|
||||
save: false,
|
||||
reloadCompose: false
|
||||
};
|
||||
let fqdnEl: any = null;
|
||||
let forceSave = false;
|
||||
let isPublicRepository = application.settings.isPublicRepository;
|
||||
let isPublicRepository = application.settings?.isPublicRepository;
|
||||
let apiUrl = application.gitSource.apiUrl;
|
||||
let branch = application.branch;
|
||||
let repository = application.repository;
|
||||
let debug = application.settings.debug;
|
||||
let previews = application.settings.previews;
|
||||
let dualCerts = application.settings.dualCerts;
|
||||
let isCustomSSL = application.settings.isCustomSSL;
|
||||
let autodeploy = application.settings.autodeploy;
|
||||
let isBot = application.settings.isBot;
|
||||
let isDBBranching = application.settings.isDBBranching;
|
||||
let debug = application.settings?.debug;
|
||||
let previews = application.settings?.previews;
|
||||
let dualCerts = application.settings?.dualCerts;
|
||||
let isCustomSSL = application.settings?.isCustomSSL;
|
||||
let autodeploy = application.settings?.autodeploy;
|
||||
let isBot = application.settings?.isBot;
|
||||
let isDBBranching = application.settings?.isDBBranching;
|
||||
let htmlUrl = application.gitSource.htmlUrl;
|
||||
|
||||
let dockerComposeFile = JSON.parse(application.dockerComposeFile) || null;
|
||||
@@ -102,7 +105,6 @@
|
||||
label: 'Uvicorn'
|
||||
}
|
||||
];
|
||||
|
||||
function normalizeDockerServices(services: any[]) {
|
||||
const tempdockerComposeServices = [];
|
||||
for (const [name, data] of Object.entries(services)) {
|
||||
@@ -237,8 +239,8 @@
|
||||
}
|
||||
}
|
||||
async function handleSubmit(toast: boolean = true) {
|
||||
if (loading) return;
|
||||
if (toast) loading = true;
|
||||
if (loading.save) return;
|
||||
if (toast) loading.save = true;
|
||||
try {
|
||||
nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
|
||||
if (application.deploymentType)
|
||||
@@ -299,7 +301,7 @@
|
||||
}
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
loading.save = false;
|
||||
}
|
||||
}
|
||||
async function selectWSGI(event: any) {
|
||||
@@ -361,6 +363,8 @@
|
||||
});
|
||||
}
|
||||
async function reloadCompose() {
|
||||
if (loading.reloadCompose) return;
|
||||
loading.reloadCompose = true;
|
||||
try {
|
||||
if (application.gitSource.type === 'github') {
|
||||
const headers = isPublicRepository
|
||||
@@ -427,6 +431,8 @@
|
||||
});
|
||||
} catch (error) {
|
||||
errorNotification(error);
|
||||
} finally {
|
||||
loading.reloadCompose = false;
|
||||
}
|
||||
}
|
||||
$: if ($status.application.statuses) {
|
||||
@@ -464,10 +470,10 @@
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
type="submit"
|
||||
class:loading
|
||||
class:loading={loading.save}
|
||||
class:bg-orange-600={forceSave}
|
||||
class:hover:bg-orange-400={forceSave}
|
||||
disabled={loading}>{$t('forms.save')}</button
|
||||
disabled={loading.save}>{$t('forms.save')}</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -569,13 +575,13 @@
|
||||
<input
|
||||
bind:this={fqdnEl}
|
||||
class="w-full"
|
||||
required={!application.settings.isBot}
|
||||
required={!application.settings?.isBot}
|
||||
readonly={isDisabled}
|
||||
disabled={isDisabled}
|
||||
name="fqdn"
|
||||
id="fqdn"
|
||||
class:border={!application.settings.isBot && !application.fqdn}
|
||||
class:border-red-500={!application.settings.isBot && !application.fqdn}
|
||||
class:border={!application.settings?.isBot && !application.fqdn}
|
||||
class:border-red-500={!application.settings?.isBot && !application.fqdn}
|
||||
bind:value={application.fqdn}
|
||||
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
|
||||
placeholder="eg: https://coollabs.io"
|
||||
@@ -714,7 +720,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
{#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">
|
||||
<Setting
|
||||
id="isDBBranching"
|
||||
@@ -993,8 +999,11 @@
|
||||
<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
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
class:loading={loading.reloadCompose}
|
||||
disabled={loading.reloadCompose}
|
||||
on:click|preventDefault={reloadCompose}>Reload Docker Compose File</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ params, stuff, url }) => {
|
||||
try {
|
||||
const { application } = stuff;
|
||||
const response = await get(`/applications/${params.id}/storages`);
|
||||
return {
|
||||
props: {
|
||||
application,
|
||||
...response
|
||||
}
|
||||
};
|
||||
@@ -19,12 +21,31 @@
|
||||
|
||||
<script lang="ts">
|
||||
export let persistentStorages: any;
|
||||
export let application: any;
|
||||
import { page } from '$app/stores';
|
||||
import Storage from './_Storage.svelte';
|
||||
import { get } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
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;
|
||||
async function refreshStorage() {
|
||||
const data = await get(`/applications/${id}/storages`);
|
||||
@@ -34,20 +55,40 @@
|
||||
|
||||
<div class="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="title font-bold pb-3">
|
||||
Persistent Volumes <Explainer
|
||||
position="dropdown-bottom"
|
||||
explanation={$t('application.storage.persistent_storage_explainer')}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
|
||||
<div class="title font-bold pb-3">Persistent Volumes</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}
|
||||
{#key storage.id}
|
||||
<Storage on:refresh={refreshStorage} {storage} />
|
||||
{/key}
|
||||
{/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 />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -38,6 +38,11 @@
|
||||
import Wordpress from './_Services/wordpress.svelte';
|
||||
|
||||
const { id } = $page.params;
|
||||
let hostPorts = Object.keys(template).filter((key) => {
|
||||
if (template[key]?.hostPorts?.length > 0) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
$: isDisabled =
|
||||
!$appSession.isAdmin ||
|
||||
$status.service.overallStatus === 'degraded' ||
|
||||
@@ -291,7 +296,7 @@
|
||||
/>
|
||||
</div>
|
||||
{: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}
|
||||
</div>
|
||||
|
||||
@@ -389,22 +394,24 @@
|
||||
on:click={() => !$status.service.isRunning && changeSettings('dualCerts')}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="exposePort"
|
||||
>Exposed Port <Explainer
|
||||
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"
|
||||
readonly={isDisabled}
|
||||
disabled={isDisabled}
|
||||
name="exposePort"
|
||||
id="exposePort"
|
||||
bind:value={service.exposePort}
|
||||
placeholder="12345"
|
||||
/>
|
||||
</div>
|
||||
{#if hostPorts.length === 0}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="exposePort"
|
||||
>Exposed Port <Explainer
|
||||
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"
|
||||
readonly={isDisabled}
|
||||
disabled={isDisabled}
|
||||
name="exposePort"
|
||||
id="exposePort"
|
||||
bind:value={service.exposePort}
|
||||
placeholder="12345"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="pt-6">
|
||||
{#each Object.keys(template) as oneService}
|
||||
@@ -444,6 +451,16 @@
|
||||
placeholder={variable.placeholder}
|
||||
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'}
|
||||
<CopyPasswordField
|
||||
disabled
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
<div class="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">
|
||||
Persistent Volumes <Explainer
|
||||
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",
|
||||
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
|
||||
"version": "3.11.7",
|
||||
"version": "3.11.11",
|
||||
"license": "Apache-2.0",
|
||||
"repository": "github:coollabsio/coolify",
|
||||
"scripts": {
|
||||
|
||||
Reference in New Issue
Block a user