saving things

This commit is contained in:
Andras Bacsai 2022-10-21 15:51:32 +02:00
parent 049d5166e8
commit 5d60b5eb8b
12 changed files with 1681 additions and 207 deletions

View File

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

View File

@ -426,6 +426,7 @@ model ServiceSetting {
serviceId String
name String
value String
variableName String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])

View File

@ -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.json', JSON.stringify([{ ...newTemplate }], null, 2))

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -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,58 +124,88 @@ 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,89 +247,42 @@ 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_')) {
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')) {
const length = variable.defaultValue.replace('$$generate_password(', '').replace('\)', '') || 16
variable.value = generatePassword({ length: Number(length) });
variable.value = generatePassword({ 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 = generatePassword({ length, isHex: true });
} else if (variable.defaultValue.startsWith('$$generate_username')) {
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 || ''
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 } } }
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 } } }
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) {
@ -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.' }

View File

@ -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;
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;
const isRunning = true;
if (fqdn) {
data.services.push({
id: oneService,
publicPort,
fqdn,
dualCerts,
configuration
});
}
}
}
}
}
}
}
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 (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;
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)
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
});
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);

View File

@ -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,7 +260,7 @@
<button
disabled={!$isDeploymentEnabled}
class="btn btn-sm gap-2"
on:click={() => startService()}
on:click={() => restartService()}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -322,12 +326,12 @@
</svg> Stop
</button>
{:else if $status.service.overallStatus === 'stopped'}
{#if $status.service.overallStatus === 'degraded'}
<button
class="btn btn-sm gap-2"
disabled={!$isDeploymentEnabled}
on:click={() => startService()}
on:click={() => restartService()}
>
{#if $status.application.overallStatus === 'degraded'}
<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,9 +370,9 @@
<path d="M7 4v16l13 -8z" />
</svg>
Deploy
{/if}
</button>
{/if}
{/if}
</div>
</div>

View File

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

View File

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

View File

@ -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
>
{/each}
{#if template[service].name}
{template[service].name || ''} <br /><span class="text-xs">({service})</span>
{:else}
<span>{service}</span>
{/if}
</div>
</button>
{/each}
</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}