Compare commits

...

36 Commits

Author SHA1 Message Date
Andras Bacsai
3bd9f00268 Merge pull request #741 from coollabsio/next
v3.11.11
2022-11-21 22:03:07 +01:00
Andras Bacsai
1aadda735d fix: webhook traefik 2022-11-21 21:58:07 +01:00
Andras Bacsai
12035208e2 fix: replace $$generate vars 2022-11-21 21:54:21 +01:00
Andras Bacsai
df8a9f673c fix: gh actions 2022-11-18 14:49:20 +01:00
Andras Bacsai
aa5c8a2c56 fix: gh actions 2022-11-18 14:48:31 +01:00
Andras Bacsai
a84540e6bb fix: gitea icon is svg 2022-11-18 14:47:23 +01:00
Andras Bacsai
fb91b64063 Merge pull request #730 from quiint/patch-1
Create Gitea icon
2022-11-18 14:45:01 +01:00
Andras Bacsai
94cc77ebca feat: only show expose if no proxy conf defined in template 2022-11-18 14:33:58 +01:00
Andras Bacsai
aac6981304 fix: no variables in template
feat: hostPort proxy conf from template
2022-11-18 14:28:05 +01:00
Andras Bacsai
ca05828b68 ga fixes 2022-11-18 11:21:41 +01:00
Andras Bacsai
8ec6b4c59c ga fixes 2022-11-18 11:19:15 +01:00
Andras Bacsai
f1be5f5341 ga fixes 2022-11-18 11:17:04 +01:00
Andras Bacsai
714c264002 fluentbit github release 2022-11-18 11:07:52 +01:00
Andras Bacsai
eca58097ef Merge pull request #733 from coollabsio/next
v3.11.10
2022-11-16 14:24:54 +01:00
Andras Bacsai
281146e22b chore: version++ 2022-11-16 12:46:29 +00:00
Andras Bacsai
f3a19a5d02 fix: wrong template/type 2022-11-16 12:40:44 +00:00
Andras Bacsai
9b9b6937f4 fix: local dev api/ws urls 2022-11-16 12:40:28 +00:00
Andras Bacsai
f54c0b7dff fix: isBot issue 2022-11-15 19:13:46 +00:00
Quiint
36c58ad286 Create gitea.svg 2022-11-14 09:54:46 -05:00
Andras Bacsai
a67f633259 Merge pull request #726 from coollabsio/next
v3.11.8
2022-11-14 14:24:52 +01:00
Andras Bacsai
f39a607c1a fix: default icon for new services 2022-11-14 13:54:06 +01:00
Andras Bacsai
0cc67ed2e5 update embeded templates 2022-11-14 13:46:17 +01:00
Andras Bacsai
5f8402c645 Merge pull request #727 from ksmithdev/main
Create keycloak.png
2022-11-14 12:59:29 +01:00
Andras Bacsai
3ab87cd11e ui: reload compose loading 2022-11-14 11:53:53 +01:00
Andras Bacsai
d5620d305d fix: ports for services 2022-11-14 11:49:32 +01:00
Andras Bacsai
35ebc5e842 fix: empty secrets on UI 2022-11-14 11:37:36 +01:00
Andras Bacsai
66276be1d2 fix: volume names for undefined volume names in compose 2022-11-14 11:26:12 +01:00
Andras Bacsai
47c0d522db chore: version++ 2022-11-14 11:00:25 +01:00
Andras Bacsai
b654883d1a ui: fixes 2022-11-14 10:59:19 +01:00
Andras Bacsai
b4f9d29129 fix: application persistent storage things 2022-11-14 10:40:28 +01:00
Andras Bacsai
bec6b961f3 fix: docker compose persistent volumes 2022-11-14 09:11:02 +01:00
Kyle Smith
2ce8f34306 Create keycloak.png 2022-11-11 14:03:05 -05:00
Andras Bacsai
30d1ae59ec revert: revert: revert 2022-11-11 14:25:02 +01:00
Andras Bacsai
ac7d4e3645 fix: getTemplates 2022-11-11 14:19:42 +01:00
Andras Bacsai
868c4001f6 gh action: revert 2022-11-11 14:17:53 +01:00
Andras Bacsai
e99c44d967 gh actions: update prod release flow 2022-11-11 13:41:02 +01:00
30 changed files with 714 additions and 235 deletions

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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])

View File

@@ -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();

View File

@@ -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) {

View File

@@ -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)
}
}

View File

@@ -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` })

View File

@@ -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);
}
}

View File

@@ -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 }) {

View File

@@ -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 } } } })
}

View File

@@ -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

View File

@@ -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) {

View File

@@ -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}

View File

@@ -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}}'?",

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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/`)}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -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": {