This commit is contained in:
Andras Bacsai 2022-10-28 11:54:03 +02:00
parent aa27aeafa1
commit dc626bd4f0
20 changed files with 422 additions and 97 deletions

View File

@ -1,5 +1,6 @@
- templateVersion: 1.0.0
defaultVersion: 1.0.3
type: appwrite
name: Appwrite
documentation: https://appwrite.io/docs
description: Secure Backend Server for Web, Mobile & Flutter Developers.
@ -1035,6 +1036,7 @@
The default value is 15 minutes.
- templateVersion: 1.0.0
defaultVersion: latest
type: weblate
name: Weblate
description: ''
services:
@ -1087,7 +1089,7 @@
label: Weblate Admin Password
defaultValue: $$generate_password
description: ''
showOnUI: true
showOnConfiguration: true
- id: $$config_postgres_user
main: $$id-postgresql
name: POSTGRES_USER
@ -1100,7 +1102,7 @@
label: PostgreSQL Password
defaultValue: $$generate_password
description: ''
showOnUI: true
showOnConfiguration: true
- id: $$config_postgres_db
main: $$id-postgresql
name: POSTGRES_DB
@ -1109,6 +1111,7 @@
description: ''
- templateVersion: 1.0.0
defaultVersion: 2022.10.14-1a5b0965
type: searxng
name: SearXNG
description: ''
services:
@ -1181,6 +1184,7 @@
description: ''
- templateVersion: 1.0.0
defaultVersion: v2.0.6
type: glitchtip
name: GlitchTip
description: ''
services:
@ -1344,6 +1348,7 @@
description: ''
- templateVersion: 1.0.0
defaultVersion: v2.13.0
type: hasura
name: Hasura
description: 'Instant realtime GraphQL APIs on any Postgres application, existing or new.'
services:
@ -1388,7 +1393,7 @@
label: Hasura Admin Password
defaultValue: $$generate_password
description: ''
showOnUI: true
showOnConfiguration: true
- id: $$config_postgres_user
name: POSTGRES_USER
label: PostgreSQL User
@ -1406,6 +1411,7 @@
description: ''
- templateVersion: 1.0.0
defaultVersion: postgresql-v1.38.0
type: umami
name: Umami
description: >-
Umami is a simple, easy to use, self-hosted web analytics solution. The goal
@ -1606,9 +1612,10 @@
label: Initial Admin Password
defaultValue: $$generate_password
description: ''
showOnUI: true
showOnConfiguration: true
- templateVersion: 1.0.0
defaultVersion: v0.29.1
type: meilisearch
name: MeiliSearch
description: >-
MeiliSearch is a lightning Fast, Ultra Relevant, and Typo-Tolerant Search
@ -1634,9 +1641,10 @@
label: Master Key
defaultValue: $$generate_hex(64)
description: ''
showOnUI: true
showOnConfiguration: true
- templateVersion: 1.0.0
defaultVersion: latest
type: ghost
name: Ghost
description: >-
Ghost is a free and open source blogging platform written in JavaScript and
@ -1749,8 +1757,9 @@
description: ''
- templateVersion: 1.0.0
defaultVersion: php8.1
name: WordPress
description: WordPress is a content management system based on PHP.
type: wordpress
name: WordPress (with MySQL)
description: WordPress is a content management system based on PHP with new MySQL database.
services:
$$id:
name: WordPress
@ -1789,53 +1798,138 @@
label: WordPress DB Host
defaultValue: $$id-mysql
description: ''
readonly: true
- id: $$config_wordpress_db_user
name: WORDPRESS_DB_USER
label: WordPress DB User
defaultValue: $$config_mysql_user
description: ''
readonly: true
- id: $$secret_wordpress_db_password
name: WORDPRESS_DB_PASSWORD
label: WordPress DB Password
defaultValue: $$secret_mysql_password
description: ''
readonly: true
- id: $$config_wordpress_db_name
name: WORDPRESS_DB_NAME
label: WordPress DB Name
defaultValue: $$config_mysql_database
description: ''
readonly: true
- id: $$config_wordpress_config_extra
name: WORDPRESS_CONFIG_EXTRA
label: WordPress Config Extra
defaultValue: ''
description: ''
type: 'textarea'
placeholder: |
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);
@ini_set('display_errors', 0);
- id: $$secret_mysql_root_password
name: MYSQL_ROOT_PASSWORD
label: MySQL Root Password
defaultValue: $$generate_password
description: ''
readonly: true
- id: $$config_mysql_root_user
name: MYSQL_ROOT_USER
label: MySQL Root User
defaultValue: $$generate_username
description: ''
readonly: true
- id: $$config_mysql_database
name: MYSQL_DATABASE
label: MySQL Database
defaultValue: wordpress
description: ''
readonly: true
- id: $$config_mysql_user
name: MYSQL_USER
label: MySQL User
defaultValue: $$generate_username
description: ''
readonly: true
- id: $$secret_mysql_password
name: MYSQL_PASSWORD
label: MySQL Password
defaultValue: $$generate_password
description: ''
readonly: true
- templateVersion: 1.0.0
defaultVersion: php8.1
type: wordpress-only
name: WordPress (without MySQL)
description: WordPress is a content management system based on PHP without MySQL.
services:
$$id:
name: WordPress
documentation: 'Taken from https://docs.docker.com/compose/wordpress/'
image: 'wordpress:$$core_version'
volumes:
- '$$id-wordpress-data:/var/www/html'
environment:
- WORDPRESS_DB_HOST=$$config_wordpress_db_host
- WORDPRESS_DB_PORT=$$config_wordpress_db_port
- WORDPRESS_DB_USER=$$config_wordpress_db_user
- WORDPRESS_DB_PASSWORD=$$secret_wordpress_db_password
- WORDPRESS_DB_NAME=$$config_wordpress_db_name
- WORDPRESS_CONFIG_EXTRA=$$config_wordpress_config_extra
ports:
- '80'
variables:
- id: $$config_wordpress_db_host
name: WORDPRESS_DB_HOST
label: Database Host
defaultValue: ''
description: ''
placeholder: 'db.coollabs.io'
required: true
- id: $$config_wordpress_db_port
name: WORDPRESS_DB_PORT
label: Database Port
defaultValue: ''
description: ''
placeholder: '3306'
required: true
- id: $$config_wordpress_db_user
name: WORDPRESS_DB_USER
label: Database User
defaultValue: ''
description: ''
placeholder: 'wordpress'
required: true
- id: $$secret_wordpress_db_password
name: WORDPRESS_DB_PASSWORD
label: Database Password
defaultValue: ''
description: ''
placeholder: 'supers3cr3tpassw0rd!'
required: true
showOnConfiguration: true
- id: $$config_wordpress_db_name
name: WORDPRESS_DB_NAME
label: Database Name
defaultValue: ''
description: ''
placeholder: 'wordpress'
required: true
- id: $$config_wordpress_config_extra
name: WORDPRESS_CONFIG_EXTRA
label: Extra Config
defaultValue: ''
description: ''
type: 'textarea'
placeholder: |
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);
@ini_set('display_errors', 0);
- templateVersion: 1.0.0
defaultVersion: 4.7.1
type: vscodeserver
name: VSCode Server
description: >-
vscode-server by Coder is VS Code running on a remote server, accessible
@ -1861,9 +1955,10 @@
label: Password
defaultValue: $$generate_password
description: ''
showOnUI: true
showOnConfiguration: true
- templateVersion: 1.0.0
defaultVersion: RELEASE.2022-10-15T19-57-03Z
type: minio
name: MinIO
description: ' MinIO is a cloud storage server compatible with Amazon S3'
services:
@ -1915,9 +2010,10 @@
label: Root User Password
defaultValue: $$generate_password
description: ''
showOnUI: true
showOnConfiguration: true
- templateVersion: 1.0.0
defaultVersion: 0.21.1
type: fider
name: Fider
description: Fider is a platform to collect and organize customer feedback.
services:
@ -2032,6 +2128,7 @@
description: ''
- templateVersion: 1.0.0
defaultVersion: 0.198.1
type: n8nio
name: n8n.io
description: n8n is a free and open node based Workflow Automation Tool.
services:
@ -2056,6 +2153,7 @@
description: ''
- templateVersion: 1.0.0
defaultVersion: stable
type: plausibleanalytics
name: Plausible Analytics
description: Plausible is a lightweight and open-source website analytics tool.
services:
@ -2157,7 +2255,7 @@
label: Admin User Password
defaultValue: $$generate_password
description: This is the admin password. Please change it.
showOnUI: true
showOnConfiguration: true
- id: $$secret_secret_key_base
name: SECRET_KEY_BASE
label: Secret Key Base
@ -2185,7 +2283,7 @@
label: PostgreSQL Password
defaultValue: $$generate_password
description: ''
showOnUI: true
showOnConfiguration: true
- id: $$config_postgresql_database
main: $$id-postgresql
name: POSTGRESQL_DATABASE
@ -2199,6 +2297,7 @@
description: This is the default script name.
- templateVersion: 1.0.0
defaultVersion: 0.98.1
type: nocodb
name: NocoDB
description: >-
The Open Source Airtable Alternative - Turns any MySQL, PostgreSQL, SQL

View File

@ -0,0 +1,32 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Wordpress" (
"id" TEXT NOT NULL PRIMARY KEY,
"extraConfig" TEXT,
"tablePrefix" TEXT,
"ownMysql" BOOLEAN NOT NULL DEFAULT false,
"mysqlHost" TEXT,
"mysqlPort" INTEGER,
"mysqlUser" TEXT,
"mysqlPassword" TEXT,
"mysqlRootUser" TEXT,
"mysqlRootUserPassword" TEXT,
"mysqlDatabase" TEXT,
"mysqlPublicPort" INTEGER,
"ftpEnabled" BOOLEAN NOT NULL DEFAULT false,
"ftpUser" TEXT,
"ftpPassword" TEXT,
"ftpPublicPort" INTEGER,
"ftpHostKey" TEXT,
"ftpHostKeyPrivate" TEXT,
"serviceId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Wordpress_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_Wordpress" ("createdAt", "extraConfig", "ftpEnabled", "ftpHostKey", "ftpHostKeyPrivate", "ftpPassword", "ftpPublicPort", "ftpUser", "id", "mysqlDatabase", "mysqlHost", "mysqlPassword", "mysqlPort", "mysqlPublicPort", "mysqlRootUser", "mysqlRootUserPassword", "mysqlUser", "ownMysql", "serviceId", "tablePrefix", "updatedAt") SELECT "createdAt", "extraConfig", "ftpEnabled", "ftpHostKey", "ftpHostKeyPrivate", "ftpPassword", "ftpPublicPort", "ftpUser", "id", "mysqlDatabase", "mysqlHost", "mysqlPassword", "mysqlPort", "mysqlPublicPort", "mysqlRootUser", "mysqlRootUserPassword", "mysqlUser", "ownMysql", "serviceId", "tablePrefix", "updatedAt" FROM "Wordpress";
DROP TABLE "Wordpress";
ALTER TABLE "new_Wordpress" RENAME TO "Wordpress";
CREATE UNIQUE INDEX "Wordpress_serviceId_key" ON "Wordpress"("serviceId");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@ -480,10 +480,10 @@ model Wordpress {
ownMysql Boolean @default(false)
mysqlHost String?
mysqlPort Int?
mysqlUser String
mysqlPassword String
mysqlRootUser String
mysqlRootUserPassword String
mysqlUser String?
mysqlPassword String?
mysqlRootUser String?
mysqlRootUserPassword String?
mysqlDatabase String?
mysqlPublicPort Int?
ftpEnabled Boolean @default(false)

View File

@ -8,6 +8,7 @@ const template = yaml.load(templateYml)
const newTemplate = {
"templateVersion": "1.0.0",
"defaultVersion": "latest",
"type": "",
"name": "",
"description": "",
"services": {
@ -18,6 +19,7 @@ const newTemplate = {
const version = template.caproverOneClickApp.variables.find(v => v.id === '$$cap_APP_VERSION' || v.id === '$$cap_version').defaultValue || 'latest'
newTemplate.name = template.caproverOneClickApp.displayName
newTemplate.type = template.caproverOneClickApp.displayName.replaceAll(' ', '').toLowerCase()
newTemplate.documentation = template.caproverOneClickApp.documentation
newTemplate.description = template.caproverOneClickApp.description
newTemplate.defaultVersion = version

View File

@ -7,19 +7,32 @@ const templates = await fs.readFile('../devTemplates.yaml', 'utf8');
const devTemplates = yaml.load(templates);
for (const template of devTemplates) {
let image = template.services['$$id'].image.replaceAll(':$$core_version', '');
const name = template.name
if (!image.includes('/')) {
image = `library/${image}`;
}
repositories.push({ image, name: name.toLowerCase().replaceAll(' ', '') });
repositories.push({ image, name: template.type });
}
const services = []
const numberOfTags = 30;
// const semverRegex = new RegExp(/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/g)
const semverRegex = new RegExp(/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/g)
for (const repository of repositories) {
console.log('Querying', repository.name, 'at', repository.image);
let semverRegex = new RegExp(/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/g)
if (repository.name.startsWith('wordpress')) {
semverRegex = new RegExp(/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-php(0|[1-9]\d*)$/g)
}
if (repository.name.startsWith('minio')) {
semverRegex = new RegExp(/^RELEASE.*$/g)
}
if (repository.name.startsWith('fider')) {
semverRegex = new RegExp(/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-([0-9]+)$/g)
}
if (repository.name.startsWith('searxng')) {
semverRegex = new RegExp(/^\d{4}[\.\-](0?[1-9]|[12][0-9]|3[01])[\.\-](0?[1-9]|1[012]).*$/)
}
if (repository.name.startsWith('umami')) {
semverRegex = new RegExp(/^postgresql-v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-([0-9]+)$/g)
}
if (repository.image.includes('ghcr.io')) {
const { execaCommand } = await import('execa');
const { stdout } = await execaCommand(`docker run --rm quay.io/skopeo/stable list-tags docker://${repository.image}`);

View File

@ -317,7 +317,9 @@ async function wordpress(service: any, template: any) {
]
await migrateSettings(settings, service, template);
await migrateSecrets(secrets, service);
if (ownMysql) {
await prisma.service.update({ where: { id: service.id }, data: { type: "wordpress-only" } })
}
// Remove old service data
// await prisma.service.update({ where: { id: service.id }, data: { wordpress: { delete: true } } })
}

View File

@ -42,7 +42,7 @@ export function getAPIUrl() {
if (process.env.CODESANDBOX_HOST) {
return `https://${process.env.CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`;
}
return isDev ? 'http://localhost:3001' : 'http://localhost:3000';
return isDev ? 'http://host.docker.internal:3001' : 'http://localhost:3000';
}
export function getUIUrl() {
@ -1447,12 +1447,13 @@ export async function getServiceFromDB({
persistentStorage: true,
serviceSecret: true,
serviceSetting: true,
wordpress: true
}
});
if (!body) {
return null
}
body.type = fixType(body.type);
// body.type = fixType(body.type);
if (body?.serviceSecret.length > 0) {
body.serviceSecret = body.serviceSecret.map((s) => {
@ -1460,6 +1461,9 @@ export async function getServiceFromDB({
return s;
});
}
if (body.wordpress) {
body.wordpress.ftpPassword = decrypt(body.wordpress.ftpPassword);
}
return { ...body, settings };
}

View File

@ -42,11 +42,16 @@ export async function startService(request: FastifyRequest<ServiceStartStop>) {
const template: any = await parseAndFindServiceTemplates(service, workdir, true)
const network = destinationDockerId && destinationDocker.network;
const config = {};
for (const service in template.services) {
const isWordpress = type === 'wordpress';
for (const s in template.services) {
if (isWordpress && service.wordpress.ownMysql && s.name === 'MySQL') {
console.log('skipping predefined mysql')
continue;
}
let newEnvironments = []
if (arm) {
if (template.services[service]?.environmentArm?.length > 0) {
for (const environment of template.services[service].environmentArm) {
if (template.services[s]?.environmentArm?.length > 0) {
for (const environment of template.services[s].environmentArm) {
const [env, value] = environment.split("=");
if (!value.startsWith('$$secret') && value !== '') {
newEnvironments.push(`${env}=${value}`)
@ -54,8 +59,8 @@ export async function startService(request: FastifyRequest<ServiceStartStop>) {
}
}
} else {
if (template.services[service]?.environment?.length > 0) {
for (const environment of template.services[service].environment) {
if (template.services[s]?.environment?.length > 0) {
for (const environment of template.services[s].environment) {
const [env, value] = environment.split("=");
if (!value.startsWith('$$secret') && value !== '') {
newEnvironments.push(`${env}=${value}`)
@ -68,7 +73,7 @@ export async function startService(request: FastifyRequest<ServiceStartStop>) {
for (const secret of secrets) {
const { name, value } = secret
if (value) {
const foundEnv = !!template.services[service].environment?.find(env => env.startsWith(`${name}=`))
const foundEnv = !!template.services[s].environment?.find(env => env.startsWith(`${name}=`))
const foundNewEnv = !!newEnvironments?.find(env => env.startsWith(`${name}=`))
if (foundEnv && !foundNewEnv) {
newEnvironments.push(`${name}=${decrypt(value)}`)
@ -76,7 +81,7 @@ export async function startService(request: FastifyRequest<ServiceStartStop>) {
}
}
const customVolumes = await prisma.servicePersistentStorage.findMany({ where: { serviceId: id } })
let volumes = arm ? template.services[service].volumesArm : template.services[service].volumes
let volumes = arm ? template.services[s].volumesArm : template.services[s].volumes
if (customVolumes.length > 0) {
for (const customVolume of customVolumes) {
const { volumeName, path, containerId } = customVolume
@ -85,41 +90,58 @@ export async function startService(request: FastifyRequest<ServiceStartStop>) {
}
}
}
config[service] = {
container_name: service,
build: template.services[service].build || undefined,
command: template.services[service].command,
entrypoint: template.services[service]?.entrypoint,
image: arm ? template.services[service].imageArm : template.services[service].image,
expose: template.services[service].ports,
if (isWordpress) {
const { wordpress: { mysqlHost, mysqlPort, mysqlUser, mysqlPassword, mysqlDatabase, ownMysql } } = service
console.log({ mysqlHost, mysqlPort, mysqlUser, mysqlPassword, mysqlDatabase, ownMysql })
if (ownMysql) {
let envsToRemove = ['WORDPRESS_DB_HOST', 'WORDPRESS_DB_USER', 'WORDPRESS_DB_PASSWORD', 'WORDPRESS_DB_NAME']
for (const env of newEnvironments) {
if (envsToRemove.includes(env.split('=')[0])) {
console.log('removing', env)
newEnvironments = newEnvironments.filter(e => e !== env)
}
}
newEnvironments.push(`WORDPRESS_DB_HOST=${mysqlHost}:${mysqlPort}`)
newEnvironments.push(`WORDPRESS_DB_USER=${mysqlUser}`)
newEnvironments.push(`WORDPRESS_DB_PASSWORD=${mysqlPassword}`)
newEnvironments.push(`WORDPRESS_DB_NAME=${mysqlDatabase}`)
}
}
config[s] = {
container_name: s,
build: template.services[s].build || undefined,
command: template.services[s].command,
entrypoint: template.services[s]?.entrypoint,
image: arm ? template.services[s].imageArm : template.services[s].image,
expose: template.services[s].ports,
...(exposePort ? { ports: [`${exposePort}:${exposePort}`] } : {}),
volumes,
environment: newEnvironments,
depends_on: template.services[service]?.depends_on,
ulimits: template.services[service]?.ulimits,
cap_drop: template.services[service]?.cap_drop,
cap_add: template.services[service]?.cap_add,
depends_on: template.services[s]?.depends_on,
ulimits: template.services[s]?.ulimits,
cap_drop: template.services[s]?.cap_drop,
cap_add: template.services[s]?.cap_add,
labels: makeLabelForServices(type),
...defaultComposeConfiguration(network),
}
// Generate files for builds
if (template.services[service]?.files?.length > 0) {
if (!template.services[service].build) {
template.services[service].build = {
if (template.services[s]?.files?.length > 0) {
if (!template.services[s].build) {
template.services[s].build = {
context: workdir,
dockerfile: `Dockerfile.${service}`
dockerfile: `Dockerfile.${s}`
}
}
let Dockerfile = `
FROM ${template.services[service].image}`
for (const file of template.services[service].files) {
FROM ${template.services[s].image}`
for (const file of template.services[s].files) {
const { source, destination, content } = file;
await fs.writeFile(source, content);
Dockerfile += `
COPY ./${path.basename(source)} ${destination}`
}
await fs.writeFile(`${workdir}/Dockerfile.${service}`, Dockerfile);
await fs.writeFile(`${workdir}/Dockerfile.${servsice}`, Dockerfile);
}
}
const { volumeMounts } = persistentVolumes(id, persistentStorage, config)

View File

@ -113,7 +113,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 => fixType(t.name) === service.type)
const foundTemplate = templates.find(t => fixType(t.type) === service.type)
let parsedTemplate = {}
if (foundTemplate) {
if (!isDeploy) {
@ -130,16 +130,21 @@ export async function parseAndFindServiceTemplates(service: any, workdir?: strin
for (const env of value.environment) {
const [envKey, envValue] = env.split('=')
const variable = foundTemplate.variables.find(v => v.name === envKey) || foundTemplate.variables.find(v => v.id === envValue)
const id = variable.id.replaceAll('$$', '')
const label = variable?.label
const description = variable?.description
const defaultValue = variable?.defaultValue
const main = variable?.main || '$$id'
if (envValue.startsWith('$$config') || variable?.showOnUI) {
const type = variable?.type || 'input'
const placeholder = variable?.placeholder || ''
const readonly = variable?.readonly || false
const required = variable?.required || false
if (envValue.startsWith('$$config') || variable?.showOnConfiguration) {
if (envValue.startsWith('$$config_coolify')) {
continue
}
parsedTemplate[realKey].environment.push(
{ name: envKey, value: envValue, main, label, description, defaultValue }
{ id, name: envKey, value: envValue, main, label, description, defaultValue, type, placeholder, required, readonly }
)
}
}
@ -203,6 +208,9 @@ export async function parseAndFindServiceTemplates(service: any, workdir?: strin
if (value) {
strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, bcrypt.hashSync(value, 10) + "\"")
strParsedTemplate = strParsedTemplate.replaceAll(regex, value + "\"")
} else {
strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, "\"")
strParsedTemplate = strParsedTemplate.replaceAll(regex, "\"")
}
}
}
@ -249,7 +257,7 @@ export async function saveServiceType(request: FastifyRequest<SaveServiceType>,
const { id } = request.params;
const { type } = request.body;
const templates = await getTemplates()
let foundTemplate = templates.find(t => fixType(t.name) === fixType(type))
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) {
@ -307,6 +315,10 @@ export async function saveServiceType(request: FastifyRequest<SaveServiceType>,
}
}
await prisma.service.update({ where: { id }, data: { type, version: foundTemplate.defaultVersion, templateVersion: foundTemplate.templateVersion } })
if (type.startsWith('wordpress')) {
await prisma.service.update({ where: { id }, data: { wordpress: { create: {} } } })
}
return reply.code(201).send()
} else {
throw { status: 404, message: 'Service type not found.' }
@ -480,7 +492,7 @@ export async function saveService(request: FastifyRequest<SaveService>, reply: F
}
const templates = await getTemplates()
const service = await prisma.service.findUnique({ where: { id } })
const foundTemplate = templates.find(t => fixType(t.name) === fixType(service.type))
const foundTemplate = templates.find(t => fixType(t.type) === fixType(service.type))
for (const setting of serviceSetting) {
let { id: settingId, name, value, changed = false, isNew = false, variableName } = setting
if (changed) {
@ -506,11 +518,19 @@ export async function saveService(request: FastifyRequest<SaveService>, reply: F
export async function getServiceSecrets(request: FastifyRequest<OnlyId>) {
try {
const { id } = request.params
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
let secrets = await prisma.serviceSecret.findMany({
where: { serviceId: id },
orderBy: { createdAt: 'desc' }
});
const templates = await getTemplates()
const foundTemplate = templates.find(t => fixType(t.type) === service.type)
secrets = secrets.map((secret) => {
const foundVariable = foundTemplate?.variables.find(v => v.name === secret.name) || null
if (foundVariable) {
secret.readonly = foundVariable.readonly
}
secret.value = decrypt(secret.value);
return secret;
});

View File

@ -381,7 +381,7 @@ export async function traefikConfiguration(request, reply) {
} = service;
if (destinationDockerId) {
const templates = await getTemplates();
let found = templates.find((a) => fixType(a.name) === fixType(type));
let found = templates.find((a) => a.type === type);
if (found) {
found = JSON.parse(JSON.stringify(found).replaceAll('$$id', id));
for (const oneService of Object.keys(found.services)) {
@ -509,7 +509,7 @@ export async function traefikConfiguration(request, reply) {
}
} else {
traefik.http.routers[`${id}-${port || 'default'}`] = generateHttpRouter(`${id}-${port || 'default'}`, nakedDomain, pathPrefix)
traefik.http.routers[`${id}-${port || 'default'}-secure`] = generateProtocolRedirectRouter(`${id}-${port || 'default'}-secure`, nakedDomain, pathPrefix, 'https-to-http')
traefik.http.routers[`${id}-${port || 'default'}-secure`] = generateProtocolRedirectRouter(`${id}-${port || 'default'}`, nakedDomain, pathPrefix, 'https-to-http')
traefik.http.services[`${id}-${port || 'default'}`] = generateLoadBalancerService(id, port)
if (!dualCerts) {
@ -873,7 +873,7 @@ export async function remoteTraefikConfiguration(request: FastifyRequest<OnlyId>
} = service;
if (destinationDockerId) {
const templates = await getTemplates();
let found = templates.find((a) => fixType(a.name) === fixType(type));
let found = templates.find((a) => a.type === type);
if (found) {
const port = found.ports.main;
const publicPort = service[type]?.publicPort;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2,7 +2,12 @@
export let type: string;
export let isAbsolute = true;
import * as Icons from '$lib/components/svg/services';
const name: any = type && type[0].toUpperCase() + type.substring(1).toLowerCase();
const name: any =
type &&
(type[0].toUpperCase() + type.substring(1).toLowerCase())
.replaceAll('.', '')
.replaceAll(' ', '')
.split('-')[0];
</script>
<svelte:component this={Icons[name.replace('.','').replaceAll(' ','')]} {isAbsolute} />
<svelte:component this={Icons[name]} {isAbsolute} />

View File

@ -41,7 +41,7 @@
"saving": "Saving...",
"name": "Name",
"value": "Value",
"action": "Action",
"action": "Actions",
"is_required": "is required.",
"add": "Add",
"set": "Set",

View File

@ -1,6 +1,7 @@
<script lang="ts">
export let name = '';
export let value = '';
export let readonly = false;
export let isNewSecret = false;
import { page } from '$app/stores';
@ -55,19 +56,20 @@
bind:value={name}
required
placeholder="EXAMPLE_VARIABLE"
readonly={!isNewSecret}
readonly={!isNewSecret || readonly}
class="w-full"
class:bg-coolblack={!isNewSecret}
class:border={!isNewSecret}
class:border-dashed={!isNewSecret}
class:border-coolgray-300={!isNewSecret}
class:cursor-not-allowed={!isNewSecret}
/>
</td>
<td>
<CopyPasswordField
id={isNewSecret ? 'secretValue' : 'secretValueNew'}
name={isNewSecret ? 'secretValue' : 'secretValueNew'}
disabled={readonly}
{readonly}
isPasswordField={true}
bind:value
placeholder="J$#@UIO%HO#$U%H"
@ -80,7 +82,7 @@
<div class="flex items-center justify-center">
<button class="btn btn-sm btn-primary" on:click={() => saveSecret(true)}>Add</button>
</div>
{:else}
{:else if !readonly}
<div class="flex flex-row justify-center space-x-2">
<div class="flex items-center justify-center">
<button class="btn btn-sm btn-primary" on:click={() => saveSecret(false)}>Set</button>

View File

@ -0,0 +1,86 @@
<script lang="ts">
import { post } from '$lib/api';
import { page } from '$app/stores';
import { status } from '$lib/store';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Setting from '$lib/components/Setting.svelte';
import { browser } from '$app/env';
import { errorNotification, getDomain } from '$lib/common';
export let service: any;
const { id } = $page.params;
const settings = service.settings;
const { ipv4, ipv6 } = settings;
let ftpUrl = generateUrl(service.wordpress?.ftpPublicPort) || '';
let ftpUser = service.wordpress?.ftpUser;
let ftpPassword = service.wordpress?.ftpPassword;
let ftpLoading = false;
let ftpEnabled = service.wordpress?.ftpEnabled || false;
function generateUrl(publicPort: any) {
return browser
? `sftp://${settings?.fqdn ? getDomain(settings.fqdn) : ipv4 || ipv6}:${publicPort}`
: 'Loading...';
}
async function changeSettings(name: any) {
if (ftpLoading) return;
if ($status.service.overallStatus === 'healthy') {
ftpLoading = true;
if (name === 'ftpEnabled') {
ftpEnabled = !ftpEnabled;
}
try {
const {
publicPort,
ftpUser: user,
ftpPassword: password
} = await post(`/services/${id}/wordpress/ftp`, {
ftpEnabled
});
ftpUrl = generateUrl(publicPort);
ftpUser = user;
ftpPassword = password;
service.wordpress.ftpEnabled = ftpEnabled;
} catch (error) {
return errorNotification(error);
} finally {
ftpLoading = false;
}
}
}
</script>
<div class="grid grid-cols-2 items-center">
<Setting
id="ftpEnabled"
bind:setting={ftpEnabled}
loading={ftpLoading}
disabled={$status.service.overallStatus !== 'healthy'}
on:click={() => changeSettings('ftpEnabled')}
title="Enable sFTP connection to WordPress data"
description="Enables an on-demand sFTP connection to the WordPress data directory. This is useful if you want to use sFTP to upload files."
/>
</div>
{#if service.wordpress?.ftpEnabled}
<div class="grid grid-cols-2 items-center">
<label for="ftpUrl">sFTP Connection URI</label>
<CopyPasswordField id="ftpUrl" readonly disabled name="ftpUrl" value={ftpUrl} />
</div>
<div class="grid grid-cols-2 items-center">
<label for="ftpUser">User</label>
<CopyPasswordField id="ftpUser" readonly disabled name="ftpUser" value={ftpUser} />
</div>
<div class="grid grid-cols-2 items-center">
<label for="ftpPassword">Password</label>
<CopyPasswordField
id="ftpPassword"
isPasswordField
readonly
disabled
name="ftpPassword"
value={ftpPassword}
/>
</div>
{/if}

View File

@ -41,7 +41,7 @@
async function handleSubmit(service: any) {
try {
await post(`/services/${id}/configuration/type`, { type: service.name });
await post(`/services/${id}/configuration/type`, { type: service.type });
return await goto(from || `/services/${id}`);
} catch (error) {
return errorNotification(error);
@ -50,8 +50,8 @@
function doSearch() {
filteredServices = services.filter(
(service: any) =>
service.name.toLowerCase().includes(search.toLowerCase()) ||
service.labels?.some((label: string) => label.toLowerCase().includes(search.toLowerCase()))
service.name.toLowerCase().includes(search.toLowerCase()) ||
service.labels?.some((label: string) => label.toLowerCase().includes(search.toLowerCase()))
);
}
function cleanupSearch() {
@ -62,6 +62,7 @@
<div class="container lg:mx-auto lg:p-0 px-8 pt-5">
<div class="input-group flex w-full">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="btn btn-square cursor-default no-animation hover:bg-error" on:click={cleanupSearch}>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -91,15 +92,15 @@
<div class="container lg:mx-auto lg:pt-20 lg:p-0 px-8 pt-20">
<div class="flex flex-wrap justify-center gap-8">
{#each filteredServices as service}
{#key service.name}
<div class="p-2">
<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.name}
</button>
</form>
</div>
{#key service.name}
<div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(service)}>
<button type="submit" class="box-selection relative text-xl font-bold hover:bg-primary">
<ServiceIcons type={service.type} />
{service.name}
</button>
</form>
</div>
{/key}
{/each}
</div>

View File

@ -35,6 +35,7 @@
import ServiceStatus from '$lib/components/ServiceStatus.svelte';
import { saveForm } from './utils';
import Select from 'svelte-select';
import Wordpress from './_Services/wordpress.svelte';
const { id } = $page.params;
$: isDisabled =
@ -86,7 +87,7 @@
exposePort: service.exposePort
});
for (const setting of service.serviceSetting) {
if (setting.variableName.startsWith('$$coolify_fqdn') && setting.value) {
if (setting.variableName?.startsWith('$$coolify_fqdn') && setting.value) {
for (let field of formData) {
const [key, value] = field;
if (setting.name === key) {
@ -413,7 +414,6 @@
{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">
@ -434,6 +434,8 @@
name={variable.name}
id={variable.name}
value={service.fqdn}
placeholder={variable.placeholder}
required={variable?.required}
/>
{:else if variable.defaultValue === '$$generate_domain'}
<CopyPasswordField
@ -442,6 +444,8 @@
name={variable.name}
id={variable.name}
value={getDomain(service.fqdn) || ''}
placeholder={variable.placeholder}
required={variable?.required}
/>
{:else if variable.defaultValue === '$$generate_network'}
<CopyPasswordField
@ -450,6 +454,8 @@
name={variable.name}
id={variable.name}
value={service.destinationDocker.network}
placeholder={variable.placeholder}
required={variable?.required}
/>
{:else if variable.defaultValue === 'true' || variable.defaultValue === 'false'}
{#if variable.value === 'true' || variable.value === 'false'}
@ -461,6 +467,8 @@
name={variable.name}
bind:value={variable.value}
form="saveForm"
placeholder={variable.placeholder}
required={variable?.required}
>
<option value="true">enabled</option>
<option value="false">disabled</option>
@ -474,6 +482,8 @@
name={variable.name}
bind:value={variable.defaultValue}
form="saveForm"
placeholder={variable.placeholder}
required={variable?.required}
>
<option value="true">true</option>
<option value="false"> false</option>
@ -487,21 +497,40 @@
name={variable.name}
id={variable.name}
value={variable.value}
placeholder={variable.placeholder}
required={variable?.required}
/>
{:else if variable.type === 'textarea'}
<textarea
class="w-full"
value={variable.value}
readonly={isDisabled}
disabled={isDisabled}
class:resize-none={$status.service.overallStatus === 'healthy'}
rows="5"
name={variable.name}
id={variable.name}
placeholder={variable.placeholder}
required={variable?.required}
/>
{:else}
<CopyPasswordField
placeholder={variable.defaultValue || 'optional'}
isPasswordField={variable.id.startsWith('secret')}
required={variable?.required}
readonly={isDisabled}
disabled={isDisabled}
readonly={variable.readonly || isDisabled}
disabled={variable.readonly || isDisabled}
name={variable.name}
id={variable.name}
value={variable.value}
placeholder={variable.placeholder}
/>
{/if}
</div>
{/if}
{/each}
{#if template[oneService].name.toLowerCase() === 'wordpress' && service.type.startsWith('wordpress')}
<Wordpress {service} />
{/if}
{/if}
</div>
{/each}

View File

@ -20,7 +20,6 @@
<script lang="ts">
export let secrets: any;
export let service: any;
import Secret from './_Secret.svelte';
import { page } from '$app/stores';
import { get } from '$lib/api';
@ -84,7 +83,7 @@
{#each secrets as secret}
{#key secret.id}
<tr>
<Secret name={secret.name} value={secret.value} on:refresh={refreshSecrets} />
<Secret name={secret.name} value={secret.value} readonly={secret.readonly} on:refresh={refreshSecrets} />
</tr>
{/key}
{/each}

View File

@ -43,27 +43,36 @@ export async function saveSecret({
export async function saveForm(formData: any, service: any) {
const settings = service.serviceSetting.map((setting: { name: string }) => setting.name);
const secrets = service.serviceSecret.map((secret: { name: string }) => secret.name);
const baseCoolifySetting = ['name', 'fqdn', 'exposePort', 'version'];
for (let field of formData) {
const [key, value] = field;
service.serviceSetting = service.serviceSetting.map((setting: any) => {
if (setting.name === key) {
setting.changed = true;
setting.value = value;
}
return setting;
});
if (!settings.includes(key) && !baseCoolifySetting.includes(key)) {
service.serviceSetting.push({
id: service.id,
if (secrets.includes(key)) {
await post(`/services/${service.id}/secrets`, {
name: key,
value: value,
isNew: true
value,
});
} else {
service.serviceSetting = service.serviceSetting.map((setting: any) => {
if (setting.name === key) {
setting.changed = true;
setting.value = value;
}
return setting;
});
if (!settings.includes(key) && !baseCoolifySetting.includes(key)) {
service.serviceSetting.push({
id: service.id,
name: key,
value: value,
isNew: true
});
}
if (baseCoolifySetting.includes(key)) {
service[key] = value;
}
}
if (baseCoolifySetting.includes(key)) {
service[key] = value;
}
}
await post(`/services/${service.id}`, { ...service });
const { service: reloadedService } = await get(`/services/${service.id}`);