saving things
This commit is contained in:
parent
049d5166e8
commit
5d60b5eb8b
@ -0,0 +1,24 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `variableName` to the `ServiceSetting` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- RedefineTables
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_ServiceSetting" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"serviceId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"value" TEXT NOT NULL,
|
||||
"variableName" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "ServiceSetting_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_ServiceSetting" ("createdAt", "id", "name", "serviceId", "updatedAt", "value") SELECT "createdAt", "id", "name", "serviceId", "updatedAt", "value" FROM "ServiceSetting";
|
||||
DROP TABLE "ServiceSetting";
|
||||
ALTER TABLE "new_ServiceSetting" RENAME TO "ServiceSetting";
|
||||
CREATE UNIQUE INDEX "ServiceSetting_serviceId_name_key" ON "ServiceSetting"("serviceId", "name");
|
||||
PRAGMA foreign_key_check;
|
||||
PRAGMA foreign_keys=ON;
|
@ -422,13 +422,14 @@ model Service {
|
||||
}
|
||||
|
||||
model ServiceSetting {
|
||||
id String @id @default(cuid())
|
||||
serviceId String
|
||||
name String
|
||||
value String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
id String @id @default(cuid())
|
||||
serviceId String
|
||||
name String
|
||||
value String
|
||||
variableName String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
|
||||
@@unique([serviceId, name])
|
||||
}
|
||||
|
@ -5,9 +5,8 @@ const template = yaml.load(templateYml)
|
||||
|
||||
const newTemplate = {
|
||||
"templateVersion": "1.0.0",
|
||||
"serviceDefaultVersion": "latest",
|
||||
"defaultVersion": "latest",
|
||||
"name": "",
|
||||
"displayName": "",
|
||||
"description": "",
|
||||
"services": {
|
||||
|
||||
@ -16,13 +15,13 @@ const newTemplate = {
|
||||
}
|
||||
const version = template.caproverOneClickApp.variables.find(v => v.id === '$$cap_APP_VERSION').defaultValue || 'latest'
|
||||
|
||||
newTemplate.displayName = template.caproverOneClickApp.displayName
|
||||
newTemplate.name = template.caproverOneClickApp.displayName.toLowerCase()
|
||||
newTemplate.name = template.caproverOneClickApp.displayName
|
||||
newTemplate.documentation = template.caproverOneClickApp.documentation
|
||||
newTemplate.description = template.caproverOneClickApp.description
|
||||
newTemplate.serviceDefaultVersion = version
|
||||
newTemplate.defaultVersion = version
|
||||
|
||||
const varSet = new Set()
|
||||
|
||||
const caproverVariables = template.caproverOneClickApp.variables
|
||||
for (const service of Object.keys(template.services)) {
|
||||
const serviceTemplate = template.services[service]
|
||||
@ -61,18 +60,20 @@ for (const service of Object.keys(template.services)) {
|
||||
|
||||
if (serviceTemplate.environment && Object.keys(serviceTemplate.environment).length > 0) {
|
||||
for (const env of Object.keys(serviceTemplate.environment)) {
|
||||
const foundCaproverVariable = caproverVariables.find((item) => item.id === serviceTemplate.environment[env])
|
||||
|
||||
let value = null;
|
||||
let defaultValue = foundCaproverVariable?.defaultValue ? foundCaproverVariable?.defaultValue.toString()?.replace('$$cap_gen_random_hex', '$$$generate_hex') : ''
|
||||
|
||||
if (serviceTemplate.environment[env].startsWith('srv-captain--$$cap_appname')) {
|
||||
continue;
|
||||
value = `$$config_${env}`.toLowerCase()
|
||||
defaultValue = serviceTemplate.environment[env].replaceAll('srv-captain--$$cap_appname', '$$$id').replace('$$cap', '').replaceAll('captain-overlay-network', `$$$config_${env}`).toLowerCase()
|
||||
} else {
|
||||
value = '$$config_' + serviceTemplate.environment[env].replaceAll('srv-captain--$$cap_appname', '$$$id').replace('$$cap', '').replaceAll('captain-overlay-network', `$$$config_${env}`).toLowerCase()
|
||||
}
|
||||
const value = '$$config_' + serviceTemplate.environment[env].replaceAll('srv-captain--$$cap_appname', '$$$id').replace('$$cap', '').replaceAll('captain-overlay-network', `$$$config_${env}`).toLowerCase()
|
||||
newService.environment.push(`${env}=${value}`)
|
||||
const foundVariable = varSet.has(env)
|
||||
if (!foundVariable) {
|
||||
const foundCaproverVariable = caproverVariables.find((item) => item.id === serviceTemplate.environment[env])
|
||||
const defaultValue = foundCaproverVariable?.defaultValue ? foundCaproverVariable?.defaultValue.toString()?.replace('$$cap_gen_random_hex', '$$$generate_hex') : ''
|
||||
if (defaultValue && defaultValue !== foundCaproverVariable?.defaultValue) {
|
||||
console.log('changed')
|
||||
}
|
||||
newTemplate.variables.push({
|
||||
"id": value,
|
||||
"name": env,
|
||||
@ -84,12 +85,23 @@ for (const service of Object.keys(template.services)) {
|
||||
varSet.add(env)
|
||||
}
|
||||
}
|
||||
|
||||
if (serviceTemplate.volumes && serviceTemplate.volumes.length > 0) {
|
||||
for (const volume of serviceTemplate.volumes) {
|
||||
const [source, target] = volume.split(':')
|
||||
if (source === '/var/run/docker.sock' || source === '/tmp') {
|
||||
continue;
|
||||
}
|
||||
newService.volumes.push(`${source.replaceAll('$$cap_appname-', '$$$id-')}:${target}`)
|
||||
}
|
||||
}
|
||||
|
||||
newTemplate.services[newServiceName] = newService
|
||||
const services = { ...newTemplate.services }
|
||||
newTemplate.services = {}
|
||||
for (const key of Object.keys(services).sort()) {
|
||||
newTemplate.services[key] = services[key]
|
||||
}
|
||||
}
|
||||
await fs.writeFile('./caprover_new.yml', yaml.dump([{ ...newTemplate }]))
|
||||
await fs.writeFile('./caprover_new.yml', yaml.dump([{ ...newTemplate }]))
|
||||
await fs.writeFile('./caprover_new.json', JSON.stringify([{ ...newTemplate }], null, 2))
|
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,7 @@ import fs from 'fs/promises';
|
||||
import yaml from 'js-yaml';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { ServiceStartStop } from '../../routes/api/v1/services/types';
|
||||
import { asyncSleep, ComposeFile, createDirectories, defaultComposeConfiguration, errorHandler, executeDockerCmd, getDomain, getFreePublicPort, getServiceFromDB, getServiceImage, getServiceMainPort, isARM, isDev, makeLabelForServices, persistentVolumes, prisma } from '../common';
|
||||
import { asyncSleep, ComposeFile, createDirectories, decrypt, defaultComposeConfiguration, errorHandler, executeDockerCmd, getDomain, getFreePublicPort, getServiceFromDB, getServiceImage, getServiceMainPort, isARM, isDev, makeLabelForServices, persistentVolumes, prisma } from '../common';
|
||||
import { defaultServiceConfigurations } from '../services';
|
||||
import { OnlyId } from '../../types';
|
||||
|
||||
@ -706,6 +706,16 @@ export async function startService(request: FastifyRequest<ServiceStartStop>) {
|
||||
if (!value.startsWith('$$secret') && value !== '') {
|
||||
newEnviroments.push(`${env}=${value}`)
|
||||
}
|
||||
|
||||
}
|
||||
const secrets = await prisma.serviceSecret.findMany({ where: { serviceId: id } })
|
||||
for (const secret of secrets) {
|
||||
const { name, value } = secret
|
||||
if (value) {
|
||||
if (template.services[service].environment.find(env => env.startsWith(`${name}=`)) && !newEnviroments.find(env => env.startsWith(`${name}=`))) {
|
||||
newEnviroments.push(`${name}=${decrypt(value)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
config[service] = {
|
||||
container_name: service,
|
||||
@ -757,7 +767,6 @@ export async function startService(request: FastifyRequest<ServiceStartStop>) {
|
||||
}
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
console.log(composeFileDestination)
|
||||
await startServiceContainers(destinationDocker.id, composeFileDestination)
|
||||
return {}
|
||||
} catch ({ status, message }) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
export default [
|
||||
{
|
||||
"templateVersion": "1.0.0",
|
||||
"serviceDefaultVersion": "latest",
|
||||
"defaultVersion": "latest",
|
||||
"name": "weblate",
|
||||
"displayName": "Weblate",
|
||||
"description": "",
|
||||
@ -99,7 +99,7 @@ export default [
|
||||
},
|
||||
{
|
||||
"templateVersion": "1.0.0",
|
||||
"serviceDefaultVersion": "2022.10.14-1a5b0965",
|
||||
"defaultVersion": "2022.10.14-1a5b0965",
|
||||
"name": "searxng",
|
||||
"displayName": "SearXNG",
|
||||
"description": "",
|
||||
@ -183,7 +183,7 @@ export default [
|
||||
},
|
||||
{
|
||||
"templateVersion": "1.0.0",
|
||||
"serviceDefaultVersion": "v2.0.6",
|
||||
"defaultVersion": "v2.0.6",
|
||||
"name": "glitchtip",
|
||||
"displayName": "GlitchTip",
|
||||
"description": "",
|
||||
@ -398,7 +398,7 @@ export default [
|
||||
},
|
||||
{
|
||||
"templateVersion": "1.0.0",
|
||||
"serviceDefaultVersion": "v2.13.0",
|
||||
"defaultVersion": "v2.13.0",
|
||||
"name": "hasura",
|
||||
"displayName": "Hasura",
|
||||
"description": "Instant realtime GraphQL APIs on any Postgres application, existing or new.",
|
||||
@ -484,7 +484,7 @@ export default [
|
||||
},
|
||||
{
|
||||
"templateVersion": "1.0.0",
|
||||
"serviceDefaultVersion": "postgresql-v1.38.0",
|
||||
"defaultVersion": "postgresql-v1.38.0",
|
||||
"name": "umami",
|
||||
"displayName": "Umami",
|
||||
"description": "Umami is a simple, easy to use, self-hosted web analytics solution. The goal is to provide you with a friendly privacy-focused alternative to Google Analytics.",
|
||||
@ -713,7 +713,7 @@ export default [
|
||||
},
|
||||
{
|
||||
"templateVersion": "1.0.0",
|
||||
"serviceDefaultVersion": "v0.29.1",
|
||||
"defaultVersion": "v0.29.1",
|
||||
"name": "meilisearch",
|
||||
"displayName": "MeiliSearch",
|
||||
"description": "MeiliSearch is a lightning Fast, Ultra Relevant, and Typo-Tolerant Search Engine",
|
||||
@ -752,7 +752,7 @@ export default [
|
||||
},
|
||||
{
|
||||
"templateVersion": "1.0.0",
|
||||
"serviceDefaultVersion": "latest",
|
||||
"defaultVersion": "latest",
|
||||
"name": "ghost",
|
||||
"displayName": "Ghost",
|
||||
"description": "Ghost is a free and open source blogging platform written in JavaScript and distributed under the MIT License",
|
||||
@ -904,7 +904,7 @@ export default [
|
||||
},
|
||||
{
|
||||
"templateVersion": "1.0.0",
|
||||
"serviceDefaultVersion": "php8.1",
|
||||
"defaultVersion": "php8.1",
|
||||
"name": "wordpress",
|
||||
"displayName": "WordPress",
|
||||
"description": "WordPress is a content management system based on PHP.",
|
||||
@ -1022,7 +1022,7 @@ export default [
|
||||
},
|
||||
{
|
||||
"templateVersion": "1.0.0",
|
||||
"serviceDefaultVersion": "4.7.1",
|
||||
"defaultVersion": "4.7.1",
|
||||
"name": "vscodeserver",
|
||||
"displayName": "VSCode Server",
|
||||
"description": "vscode-server by Coder is VS Code running on a remote server, accessible through the browser.",
|
||||
@ -1062,7 +1062,7 @@ export default [
|
||||
},
|
||||
{
|
||||
"templateVersion": "1.0.0",
|
||||
"serviceDefaultVersion": "RELEASE.2022-10-15T19-57-03Z",
|
||||
"defaultVersion": "RELEASE.2022-10-15T19-57-03Z",
|
||||
"name": "minio",
|
||||
"displayName": "MinIO",
|
||||
"description": " MinIO is a cloud storage server compatible with Amazon S3",
|
||||
@ -1132,7 +1132,7 @@ export default [
|
||||
},
|
||||
{
|
||||
"templateVersion": "1.0.0",
|
||||
"serviceDefaultVersion": "0.21.1",
|
||||
"defaultVersion": "0.21.1",
|
||||
"name": "fider",
|
||||
"displayName": "Fider",
|
||||
"description": "Fider is a platform to collect and organize customer feedback.",
|
||||
@ -1286,7 +1286,7 @@ export default [
|
||||
},
|
||||
{
|
||||
"templateVersion": "1.0.0",
|
||||
"serviceDefaultVersion": "0.198.1",
|
||||
"defaultVersion": "0.198.1",
|
||||
"name": "n8n",
|
||||
"displayName": "n8n.io",
|
||||
"description": "n8n is a free and open node based Workflow Automation Tool.",
|
||||
@ -1320,7 +1320,7 @@ export default [
|
||||
},
|
||||
{
|
||||
"templateVersion": "1.0.0",
|
||||
"serviceDefaultVersion": "stable",
|
||||
"defaultVersion": "stable",
|
||||
"name": "plausibleanalytics",
|
||||
"displayName": "PlausibleAnalytics",
|
||||
"description": "Plausible is a lightweight and open-source website analytics tool.",
|
||||
|
@ -115,7 +115,7 @@ export async function getServiceStatus(request: FastifyRequest<OnlyId>) {
|
||||
}
|
||||
export async function parseAndFindServiceTemplates(service: any, workdir?: string, isDeploy: boolean = false) {
|
||||
const templates = await getTemplates()
|
||||
const foundTemplate = templates.find(t => t.name === service.type.toLowerCase())
|
||||
const foundTemplate = templates.find(t => t.name.toLowerCase() === service.type.toLowerCase())
|
||||
let parsedTemplate = {}
|
||||
if (foundTemplate) {
|
||||
if (!isDeploy) {
|
||||
@ -124,57 +124,87 @@ export async function parseAndFindServiceTemplates(service: any, workdir?: strin
|
||||
parsedTemplate[realKey] = {
|
||||
name: value.name,
|
||||
image: value.image,
|
||||
environment: []
|
||||
environment: [],
|
||||
proxy: {}
|
||||
}
|
||||
if (value.environment?.length > 0) {
|
||||
for (const env of value.environment) {
|
||||
const [envKey, envValue] = env.split('=')
|
||||
const label = foundTemplate.variables.find(v => v.name === envKey)?.label
|
||||
const description = foundTemplate.variables.find(v => v.name === envKey)?.description
|
||||
const defaultValue = foundTemplate.variables.find(v => v.name === envKey)?.defaultValue
|
||||
const extras = foundTemplate.variables.find(v => v.name === envKey)?.extras
|
||||
const variable = foundTemplate.variables.find(v => v.name === envKey) || foundTemplate.variables.find(v => v.id === envValue)
|
||||
const label = variable?.label
|
||||
const description = variable?.description
|
||||
const defaultValue = variable?.defaultValue
|
||||
const extras = variable?.extras
|
||||
if (envValue.startsWith('$$config') || extras?.isVisibleOnUI) {
|
||||
if (envValue.startsWith('$$config_coolify')) {
|
||||
console.log({envValue,envKey})
|
||||
}
|
||||
parsedTemplate[realKey].environment.push(
|
||||
{ name: envKey, value: envValue, label, description, defaultValue, extras }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: seconday domains are not working - kinda working
|
||||
if (value?.proxy?.traefik?.configurations) {
|
||||
for (const proxyValue of value.proxy.traefik.configurations) {
|
||||
if (proxyValue.domain) {
|
||||
const variable = foundTemplate.variables.find(v => v.id === proxyValue.domain)
|
||||
if (variable) {
|
||||
const { name, label, description, defaultValue, extras } = variable
|
||||
const found = await prisma.serviceSetting.findFirst({where: {variableName: proxyValue.domain}})
|
||||
parsedTemplate[realKey].environment.push(
|
||||
{ name, value: found.value || '', label, description, defaultValue, extras }
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
parsedTemplate = foundTemplate
|
||||
}
|
||||
let strParsedTemplate = JSON.stringify(parsedTemplate)
|
||||
|
||||
// replace $$id and $$workdir
|
||||
parsedTemplate = JSON.parse(JSON.stringify(parsedTemplate).replaceAll('$$id', service.id).replaceAll('$$core_version', service.version || foundTemplate.serviceDefaultVersion))
|
||||
strParsedTemplate = strParsedTemplate.replaceAll('$$id', service.id)
|
||||
strParsedTemplate = strParsedTemplate.replaceAll('$$core_version', service.version || foundTemplate.defaultVersion)
|
||||
|
||||
// replace $$fqdn
|
||||
if (workdir) {
|
||||
parsedTemplate = JSON.parse(JSON.stringify(parsedTemplate).replaceAll('$$workdir', workdir))
|
||||
strParsedTemplate = strParsedTemplate.replaceAll('$$workdir', workdir)
|
||||
}
|
||||
|
||||
// replace $$config
|
||||
if (service.serviceSetting.length > 0) {
|
||||
for (const setting of service.serviceSetting) {
|
||||
const { name, value } = setting
|
||||
const regex = new RegExp(`\\$\\$config_${name}\\"`, 'gi')
|
||||
const { value, variableName } = setting
|
||||
if (service.fqdn && value === '$$generate_fqdn') {
|
||||
parsedTemplate = JSON.parse(JSON.stringify(parsedTemplate).replaceAll(regex, service.fqdn + "\""))
|
||||
strParsedTemplate = strParsedTemplate.replaceAll(variableName, service.fqdn)
|
||||
} else if (service.fqdn && value === '$$generate_domain') {
|
||||
parsedTemplate = JSON.parse(JSON.stringify(parsedTemplate).replaceAll(regex, getDomain(service.fqdn) + "\""))
|
||||
strParsedTemplate = strParsedTemplate.replaceAll(variableName, getDomain(service.fqdn))
|
||||
} else if (service.destinationDocker?.network && value === '$$generate_network') {
|
||||
strParsedTemplate = strParsedTemplate.replaceAll(variableName, service.destinationDocker.network)
|
||||
} else {
|
||||
parsedTemplate = JSON.parse(JSON.stringify(parsedTemplate).replaceAll(regex, value + "\""))
|
||||
strParsedTemplate = strParsedTemplate.replaceAll(variableName, value)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// replace $$secret
|
||||
if (service.serviceSecret.length > 0) {
|
||||
for (const secret of service.serviceSecret) {
|
||||
const { name, value } = secret
|
||||
parsedTemplate = JSON.parse(JSON.stringify(parsedTemplate).replaceAll(`$$hashed$$secret_${name.toLowerCase()}`, bcrypt.hashSync(value, 10)).replaceAll(`$$secret_${name.toLowerCase()}`, value))
|
||||
const regexHashed = new RegExp(`\\$\\$hashed\\$\\$secret_${name}\\"`, 'gi')
|
||||
const regex = new RegExp(`\\$\\$secret_${name}\\"`, 'gi')
|
||||
if (value) {
|
||||
strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, bcrypt.hashSync(value, 10) + "\"")
|
||||
strParsedTemplate = strParsedTemplate.replaceAll(regex, value + "\"")
|
||||
}
|
||||
}
|
||||
}
|
||||
parsedTemplate = JSON.parse(strParsedTemplate)
|
||||
}
|
||||
return parsedTemplate
|
||||
}
|
||||
@ -217,90 +247,43 @@ export async function saveServiceType(request: FastifyRequest<SaveServiceType>,
|
||||
const templates = await getTemplates()
|
||||
let foundTemplate = templates.find(t => t.name === type)
|
||||
if (foundTemplate) {
|
||||
let generatedVariables = new Set()
|
||||
let missingVariables = new Set()
|
||||
|
||||
foundTemplate = JSON.parse(JSON.stringify(foundTemplate).replaceAll('$$id', id))
|
||||
|
||||
if (foundTemplate.variables.length > 0) {
|
||||
foundTemplate.variables = foundTemplate.variables.map(variable => {
|
||||
let { id: variableId } = variable;
|
||||
if (variableId.startsWith('$$secret_')) {
|
||||
if (variable.defaultValue.startsWith('$$generate_password')) {
|
||||
const length = variable.defaultValue.replace('$$generate_password(', '').replace('\)', '') || 16
|
||||
variable.value = generatePassword({ length: Number(length) });
|
||||
} else if (variable.defaultValue.startsWith('$$generate_hex')) {
|
||||
const length = variable.defaultValue.replace('$$generate_hex(', '').replace('\)', '') || 16
|
||||
variable.value = crypto.randomBytes(Number(length)).toString('hex');
|
||||
} else if (!variable.defaultValue) {
|
||||
variable.defaultValue = undefined
|
||||
}
|
||||
}
|
||||
if (variableId.startsWith('$$config_')) {
|
||||
if (variable.defaultValue.startsWith('$$generate_username')) {
|
||||
const length = variable.defaultValue.replace('$$generate_username(', '').replace('\)', '')
|
||||
if (length !== '$$generate_username') {
|
||||
variable.value = crypto.randomBytes(Number(length)).toString('hex');
|
||||
} else {
|
||||
variable.value = cuid();
|
||||
}
|
||||
} else {
|
||||
if (variable.defaultValue.startsWith('$$generate_hex')) {
|
||||
const length = variable.defaultValue.replace('$$generate_hex(', '').replace('\)', '') || 16
|
||||
variable.value = crypto.randomBytes(Number(length)).toString('hex');
|
||||
} else {
|
||||
variable.value = variable.defaultValue || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
if (variable.value) {
|
||||
generatedVariables.add(`${variableId}=${variable.value}`)
|
||||
} else {
|
||||
missingVariables.add(variableId)
|
||||
}
|
||||
return variable
|
||||
})
|
||||
if (missingVariables.size > 0) {
|
||||
foundTemplate.variables = foundTemplate.variables.map(variable => {
|
||||
if (missingVariables.has(variable.id)) {
|
||||
variable.value = variable.defaultValue
|
||||
for (const generatedVariable of generatedVariables) {
|
||||
let [id, value] = generatedVariable.split('=')
|
||||
if (variable.value) {
|
||||
variable.value = variable.value.replaceAll(id, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
return variable
|
||||
})
|
||||
}
|
||||
|
||||
for (const variable of foundTemplate.variables) {
|
||||
if (variable.id.startsWith('$$secret_')) {
|
||||
if (!variable.value) {
|
||||
continue;
|
||||
}
|
||||
const found = await prisma.serviceSecret.findFirst({ where: { name: variable.name, serviceId: id } })
|
||||
if (!found) {
|
||||
await prisma.serviceSecret.create({
|
||||
data: { name: variable.name, value: encrypt(variable.value), service: { connect: { id } } }
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
if (variable.id.startsWith('$$config_')) {
|
||||
if (!variable.value) {
|
||||
variable.value = '';
|
||||
}
|
||||
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(), service: { connect: { id } } }
|
||||
})
|
||||
}
|
||||
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 foundTemplate.variables) {
|
||||
if (variable.id.startsWith('$$secret_')) {
|
||||
const found = await prisma.serviceSecret.findFirst({ where: { name: variable.name, serviceId: id } })
|
||||
if (!found) {
|
||||
await prisma.serviceSecret.create({
|
||||
data: { name: variable.name, value: encrypt(variable.value) || '', service: { connect: { id } } }
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
if (variable.id.startsWith('$$config_')) {
|
||||
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 } } }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
for (const service of Object.keys(foundTemplate.services)) {
|
||||
if (foundTemplate.services[service].volumes) {
|
||||
for (const volume of foundTemplate.services[service].volumes) {
|
||||
@ -316,7 +299,7 @@ export async function saveServiceType(request: FastifyRequest<SaveServiceType>,
|
||||
}
|
||||
}
|
||||
}
|
||||
await prisma.service.update({ where: { id }, data: { type, version: foundTemplate.serviceDefaultVersion } })
|
||||
await prisma.service.update({ where: { id }, data: { type, version: foundTemplate.defaultVersion } })
|
||||
return reply.code(201).send()
|
||||
} else {
|
||||
throw { status: 404, message: 'Service type not found.' }
|
||||
|
@ -4,7 +4,50 @@ import { supportedServiceTypesAndVersions } from "../../../lib/services/supporte
|
||||
import { includeServices } from "../../../lib/services/common";
|
||||
import { TraefikOtherConfiguration } from "./types";
|
||||
import { OnlyId } from "../../../types";
|
||||
import { getTemplates } from "../../../lib/services";
|
||||
|
||||
function generateLoadBalancerService(id, port) {
|
||||
return {
|
||||
loadbalancer: {
|
||||
servers: [
|
||||
{
|
||||
url: `http://${id}:${port}`
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
function generateHttpRouter(id, nakedDomain, pathPrefix) {
|
||||
return {
|
||||
entrypoints: ['web'],
|
||||
rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`${pathPrefix}\`)`,
|
||||
service: `${id}`,
|
||||
middlewares: []
|
||||
}
|
||||
}
|
||||
function generateProtocolRedirectRouter(id, nakedDomain, pathPrefix, fromTo) {
|
||||
if (fromTo === 'https-to-http') {
|
||||
return {
|
||||
entrypoints: ['websecure'],
|
||||
rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`${pathPrefix}\`)`,
|
||||
service: `${id}`,
|
||||
tls: {
|
||||
domains: {
|
||||
main: `${nakedDomain}`
|
||||
}
|
||||
},
|
||||
middlewares: ['redirect-to-http']
|
||||
}
|
||||
} else if (fromTo === 'http-to-https') {
|
||||
return {
|
||||
entrypoints: ['web'],
|
||||
rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`${pathPrefix}\`)`,
|
||||
service: `${id}`,
|
||||
middlewares: ['redirect-to-https']
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
function configureMiddleware(
|
||||
{ id, container, port, domain, nakedDomain, isHttps, isWWW, isDualCerts, scriptName, type, isCustomSSL },
|
||||
traefik
|
||||
@ -325,71 +368,127 @@ export async function traefikConfiguration(request, reply) {
|
||||
});
|
||||
|
||||
for (const service of services) {
|
||||
const {
|
||||
let {
|
||||
fqdn,
|
||||
id,
|
||||
type,
|
||||
destinationDockerId,
|
||||
dualCerts,
|
||||
serviceSetting
|
||||
} = service;
|
||||
if (destinationDockerId) {
|
||||
const found = supportedServiceTypesAndVersions.find((a) => a.name === type);
|
||||
const templates = await getTemplates();
|
||||
let found = templates.find((a) => a.name === type);
|
||||
type = type.toLowerCase();
|
||||
if (found) {
|
||||
const port = found.ports.main;
|
||||
const publicPort = service[type]?.publicPort;
|
||||
const isRunning = true;
|
||||
if (fqdn) {
|
||||
const domain = getDomain(fqdn);
|
||||
const nakedDomain = domain.replace(/^www\./, '');
|
||||
const isHttps = fqdn.startsWith('https://');
|
||||
const isWWW = fqdn.includes('www.');
|
||||
if (isRunning) {
|
||||
// Plausible Analytics custom script
|
||||
let scriptName = false;
|
||||
if (type === 'plausibleanalytics') {
|
||||
const foundScriptName = serviceSetting.find((a) => a.name === 'SCRIPT_NAME')?.value;
|
||||
if (foundScriptName) {
|
||||
scriptName = foundScriptName;
|
||||
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 { proxy: { traefik: { configurations } } } = found.services[oneService];
|
||||
for (const configuration of configurations) {
|
||||
const publicPort = service[type]?.publicPort;
|
||||
if (fqdn) {
|
||||
data.services.push({
|
||||
id: oneService,
|
||||
publicPort,
|
||||
fqdn,
|
||||
dualCerts,
|
||||
configuration
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let container = id;
|
||||
let otherDomain = null;
|
||||
let otherNakedDomain = null;
|
||||
let otherIsHttps = null;
|
||||
let otherIsWWW = null;
|
||||
if (type === 'minio') {
|
||||
const domain = service.serviceSetting.find((a) => a.name === 'MINIO_SERVER_URL')?.value
|
||||
otherDomain = getDomain(domain);
|
||||
otherNakedDomain = otherDomain.replace(/^www\./, '');
|
||||
otherIsHttps = domain.startsWith('https://');
|
||||
otherIsWWW = domain.includes('www.');
|
||||
}
|
||||
data.services.push({
|
||||
id,
|
||||
container,
|
||||
type,
|
||||
otherDomain,
|
||||
otherNakedDomain,
|
||||
otherIsHttps,
|
||||
otherIsWWW,
|
||||
port,
|
||||
publicPort,
|
||||
domain,
|
||||
nakedDomain,
|
||||
isRunning,
|
||||
isHttps,
|
||||
isWWW,
|
||||
isDualCerts: dualCerts,
|
||||
scriptName
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const service of data.services) {
|
||||
const { id, fqdn, dualCerts, configuration: { port, pathPrefix = '/' }, isCustomSSL = false } = service
|
||||
const domain = getDomain(fqdn);
|
||||
const nakedDomain = domain.replace(/^www\./, '');
|
||||
const isHttps = fqdn.startsWith('https://');
|
||||
const isWWW = fqdn.includes('www.');
|
||||
if (isHttps) {
|
||||
traefik.http.routers[id] = generateHttpRouter(id, nakedDomain, pathPrefix)
|
||||
traefik.http.routers[`${id}-secure`] = generateProtocolRedirectRouter(id, nakedDomain, pathPrefix, 'http-to-https')
|
||||
traefik.http.services[id] = generateLoadBalancerService(id, port)
|
||||
if (dualCerts) {
|
||||
traefik.http.routers[`${id}-secure`] = {
|
||||
entrypoints: ['websecure'],
|
||||
rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`${pathPrefix}\`)`,
|
||||
service: `${id}`,
|
||||
tls: isCustomSSL ? true : {
|
||||
certresolver: 'letsencrypt'
|
||||
},
|
||||
middlewares: []
|
||||
};
|
||||
} else {
|
||||
if (isWWW) {
|
||||
traefik.http.routers[`${id}-secure-www`] = {
|
||||
entrypoints: ['websecure'],
|
||||
rule: `Host(\`www.${nakedDomain}\`) && PathPrefix(\`${pathPrefix}\`)`,
|
||||
service: `${id}`,
|
||||
tls: isCustomSSL ? true : {
|
||||
certresolver: 'letsencrypt'
|
||||
},
|
||||
middlewares: []
|
||||
};
|
||||
traefik.http.routers[`${id}-secure`] = {
|
||||
entrypoints: ['websecure'],
|
||||
rule: `Host(\`${nakedDomain}\`) && PathPrefix(\`${pathPrefix}\`)`,
|
||||
service: `${id}`,
|
||||
tls: {
|
||||
domains: {
|
||||
main: `${domain}`
|
||||
}
|
||||
},
|
||||
middlewares: ['redirect-to-www']
|
||||
};
|
||||
traefik.http.routers[`${id}`].middlewares.push('redirect-to-www');
|
||||
} else {
|
||||
traefik.http.routers[`${id}-secure-www`] = {
|
||||
entrypoints: ['websecure'],
|
||||
rule: `Host(\`www.${nakedDomain}\`) && PathPrefix(\`${pathPrefix}\`)`,
|
||||
service: `${id}`,
|
||||
tls: {
|
||||
domains: {
|
||||
main: `${domain}`
|
||||
}
|
||||
},
|
||||
middlewares: ['redirect-to-non-www']
|
||||
};
|
||||
traefik.http.routers[`${id}-secure`] = {
|
||||
entrypoints: ['websecure'],
|
||||
rule: `Host(\`${domain}\`) && PathPrefix(\`${pathPrefix}\`)`,
|
||||
service: `${id}`,
|
||||
tls: isCustomSSL ? true : {
|
||||
certresolver: 'letsencrypt'
|
||||
},
|
||||
middlewares: []
|
||||
};
|
||||
traefik.http.routers[`${id}`].middlewares.push('redirect-to-non-www');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
traefik.http.routers[id] = generateHttpRouter(id, nakedDomain, pathPrefix)
|
||||
traefik.http.routers[`${id}-secure`] = generateProtocolRedirectRouter(id, nakedDomain, pathPrefix, 'https-to-http')
|
||||
traefik.http.services[id] = generateLoadBalancerService(id, port)
|
||||
|
||||
if (!dualCerts) {
|
||||
if (isWWW) {
|
||||
traefik.http.routers[`${id}`].middlewares.push('redirect-to-www');
|
||||
traefik.http.routers[`${id}-secure`].middlewares.push('redirect-to-www');
|
||||
} else {
|
||||
traefik.http.routers[`${id}`].middlewares.push('redirect-to-non-www');
|
||||
traefik.http.routers[`${id}-secure`].middlewares.push('redirect-to-non-www');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
...traefik
|
||||
}
|
||||
const { fqdn, dualCerts } = await prisma.setting.findFirst();
|
||||
if (fqdn) {
|
||||
const domain = getDomain(fqdn);
|
||||
|
@ -93,6 +93,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
async function restartService() {
|
||||
await stopService();
|
||||
await startService();
|
||||
}
|
||||
async function stopService() {
|
||||
const sure = confirm($t('database.confirm_stop', { name: service.name }));
|
||||
if (sure) {
|
||||
@ -256,14 +260,14 @@
|
||||
<button
|
||||
disabled={!$isDeploymentEnabled}
|
||||
class="btn btn-sm gap-2"
|
||||
on:click={() => startService()}
|
||||
on:click={() => restartService()}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
@ -322,12 +326,12 @@
|
||||
</svg> Stop
|
||||
</button>
|
||||
{:else if $status.service.overallStatus === 'stopped'}
|
||||
<button
|
||||
class="btn btn-sm gap-2"
|
||||
disabled={!$isDeploymentEnabled}
|
||||
on:click={() => startService()}
|
||||
>
|
||||
{#if $status.application.overallStatus === 'degraded'}
|
||||
{#if $status.service.overallStatus === 'degraded'}
|
||||
<button
|
||||
class="btn btn-sm gap-2"
|
||||
disabled={!$isDeploymentEnabled}
|
||||
on:click={() => restartService()}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6"
|
||||
@ -345,7 +349,13 @@
|
||||
/>
|
||||
</svg>
|
||||
{$status.application.statuses.length === 1 ? 'Force Redeploy' : 'Redeploy Stack'}
|
||||
{:else if $status.application.overallStatus === 'stopped'}
|
||||
</button>
|
||||
{:else if $status.service.overallStatus === 'stopped'}
|
||||
<button
|
||||
class="btn btn-sm gap-2"
|
||||
disabled={!$isDeploymentEnabled}
|
||||
on:click={() => startService()}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6 text-pink-500"
|
||||
@ -360,8 +370,8 @@
|
||||
<path d="M7 4v16l13 -8z" />
|
||||
</svg>
|
||||
Deploy
|
||||
{/if}
|
||||
</button>
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -94,7 +94,7 @@
|
||||
<form on:submit|preventDefault={() => handleSubmit(service)}>
|
||||
<button type="submit" class="box-selection relative text-xl font-bold hover:bg-primary">
|
||||
<!-- <ServiceIcons type={service.name} /> -->
|
||||
{service.displayName}
|
||||
{service.name}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -308,6 +308,14 @@
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{#each Object.keys(template) as oneService}
|
||||
{#each template[oneService].environment.filter( (a) => a.name.startsWith('COOLIFY_FQDN_') ) as variable}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label class="h-10" for={variable.name}>{variable.label || variable.name}</label>
|
||||
<input class="w-full" name={variable.name} id={variable.name} value={variable.value} />
|
||||
</div>
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
{#if forceSave}
|
||||
<div class="flex-col space-y-2 pt-4 text-center">
|
||||
@ -373,6 +381,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div />
|
||||
<div>
|
||||
{#each Object.keys(template) as oneService}
|
||||
@ -381,15 +390,18 @@
|
||||
class:border-b={template[oneService].environment.length > 0}
|
||||
class:border-coolgray-500={template[oneService].environment.length > 0}
|
||||
>
|
||||
<div class="title font-bold pb-3">{template[oneService].name || oneService.replace(`${id}-`,'').replace(id,service.type)}</div>
|
||||
<div class="title font-bold pb-3 capitalize">
|
||||
{template[oneService].name ||
|
||||
oneService.replace(`${id}-`, '').replace(id, service.type)}
|
||||
</div>
|
||||
|
||||
<ServiceStatus id={oneService} />
|
||||
</div>
|
||||
<div class="grid grid-flow-row gap-2 px-4">
|
||||
{#if template[oneService].environment.length > 0}
|
||||
{#each template[oneService].environment as variable}
|
||||
{#each template[oneService].environment.filter((a) => !a.name.startsWith('COOLIFY_FQDN_')) as variable}
|
||||
<div class="grid grid-cols-2 items-center gap-2">
|
||||
<label class="h-10" for={variable.name}>{variable.label}</label>
|
||||
<label class="h-10" for={variable.name}>{variable.label || variable.name}</label>
|
||||
{#if variable.defaultValue === '$$generate_fqdn'}
|
||||
<input
|
||||
class="w-full"
|
||||
@ -408,6 +420,15 @@
|
||||
id={variable.name}
|
||||
value={getDomain(service.fqdn)}
|
||||
/>
|
||||
{:else if variable.defaultValue === '$$generate_network'}
|
||||
<input
|
||||
class="w-full"
|
||||
disabled
|
||||
readonly
|
||||
name={variable.name}
|
||||
id={variable.name}
|
||||
value={service.destinationDocker.network}
|
||||
/>
|
||||
{:else if variable.defaultValue === 'true' || variable.defaultValue === 'false'}
|
||||
{#if variable.value === 'true' || variable.value === 'false'}
|
||||
<select
|
||||
|
@ -93,8 +93,9 @@
|
||||
<div class="title font-bold pb-3">Service Logs</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-2 lg:gap-8 pb-4">
|
||||
{#if template}
|
||||
|
||||
{#if template}
|
||||
<div class="grid grid-cols-3 gap-2 lg:gap-8 pb-4">
|
||||
{#each Object.keys(template) as service}
|
||||
<button
|
||||
on:click={() => selectService(service, true)}
|
||||
@ -102,11 +103,17 @@
|
||||
class:bg-coolgray-200={selectedService !== service}
|
||||
class="w-full rounded p-5 hover:bg-primary font-bold"
|
||||
>
|
||||
{service}</button
|
||||
>
|
||||
{#if template[service].name}
|
||||
{template[service].name || ''} <br /><span class="text-xs">({service})</span>
|
||||
{:else}
|
||||
<span>{service}</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="w-full flex justify-center font-bold text-xl">Loading components...</div>
|
||||
{/if}
|
||||
|
||||
{#if selectedService}
|
||||
<div class="flex flex-row justify-center space-x-2">
|
||||
@ -117,12 +124,6 @@
|
||||
{:else}
|
||||
<div class="relative w-full">
|
||||
<div class="flex justify-start sticky space-x-2 pb-2">
|
||||
{#if loadLogsInterval}
|
||||
<button id="streaming" class="btn btn-sm bg-transparent border-none loading"
|
||||
>Streaming logs</button
|
||||
>
|
||||
{/if}
|
||||
<div class="flex-1" />
|
||||
<button on:click={followBuild} class="btn btn-sm " class:bg-coollabs={followingLogs}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -142,6 +143,11 @@
|
||||
</svg>
|
||||
{followingLogs ? 'Following Logs...' : 'Follow Logs'}
|
||||
</button>
|
||||
{#if loadLogsInterval}
|
||||
<button id="streaming" class="btn btn-sm bg-transparent border-none loading"
|
||||
>Streaming logs</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
bind:this={logsEl}
|
||||
|
Loading…
Reference in New Issue
Block a user