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

View File

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

View File

@@ -7,19 +7,32 @@ const templates = await fs.readFile('../devTemplates.yaml', 'utf8');
const devTemplates = yaml.load(templates); const devTemplates = yaml.load(templates);
for (const template of devTemplates) { for (const template of devTemplates) {
let image = template.services['$$id'].image.replaceAll(':$$core_version', ''); let image = template.services['$$id'].image.replaceAll(':$$core_version', '');
const name = template.name
if (!image.includes('/')) { if (!image.includes('/')) {
image = `library/${image}`; image = `library/${image}`;
} }
repositories.push({ image, name: name.toLowerCase().replaceAll(' ', '') }); repositories.push({ image, name: template.type });
} }
const services = [] const services = []
const numberOfTags = 30; 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*)(?:-((?: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) { for (const repository of repositories) {
console.log('Querying', repository.name, 'at', repository.image); 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')) { if (repository.image.includes('ghcr.io')) {
const { execaCommand } = await import('execa'); const { execaCommand } = await import('execa');
const { stdout } = await execaCommand(`docker run --rm quay.io/skopeo/stable list-tags docker://${repository.image}`); 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 migrateSettings(settings, service, template);
await migrateSecrets(secrets, service); await migrateSecrets(secrets, service);
if (ownMysql) {
await prisma.service.update({ where: { id: service.id }, data: { type: "wordpress-only" } })
}
// Remove old service data // Remove old service data
// await prisma.service.update({ where: { id: service.id }, data: { wordpress: { delete: true } } }) // 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) { if (process.env.CODESANDBOX_HOST) {
return `https://${process.env.CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`; 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() { export function getUIUrl() {
@@ -1447,12 +1447,13 @@ export async function getServiceFromDB({
persistentStorage: true, persistentStorage: true,
serviceSecret: true, serviceSecret: true,
serviceSetting: true, serviceSetting: true,
wordpress: true
} }
}); });
if (!body) { if (!body) {
return null return null
} }
body.type = fixType(body.type); // body.type = fixType(body.type);
if (body?.serviceSecret.length > 0) { if (body?.serviceSecret.length > 0) {
body.serviceSecret = body.serviceSecret.map((s) => { body.serviceSecret = body.serviceSecret.map((s) => {
@@ -1460,6 +1461,9 @@ export async function getServiceFromDB({
return s; return s;
}); });
} }
if (body.wordpress) {
body.wordpress.ftpPassword = decrypt(body.wordpress.ftpPassword);
}
return { ...body, settings }; 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 template: any = await parseAndFindServiceTemplates(service, workdir, true)
const network = destinationDockerId && destinationDocker.network; const network = destinationDockerId && destinationDocker.network;
const config = {}; 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 = [] let newEnvironments = []
if (arm) { if (arm) {
if (template.services[service]?.environmentArm?.length > 0) { if (template.services[s]?.environmentArm?.length > 0) {
for (const environment of template.services[service].environmentArm) { for (const environment of template.services[s].environmentArm) {
const [env, value] = environment.split("="); const [env, value] = environment.split("=");
if (!value.startsWith('$$secret') && value !== '') { if (!value.startsWith('$$secret') && value !== '') {
newEnvironments.push(`${env}=${value}`) newEnvironments.push(`${env}=${value}`)
@@ -54,8 +59,8 @@ export async function startService(request: FastifyRequest<ServiceStartStop>) {
} }
} }
} else { } else {
if (template.services[service]?.environment?.length > 0) { if (template.services[s]?.environment?.length > 0) {
for (const environment of template.services[service].environment) { for (const environment of template.services[s].environment) {
const [env, value] = environment.split("="); const [env, value] = environment.split("=");
if (!value.startsWith('$$secret') && value !== '') { if (!value.startsWith('$$secret') && value !== '') {
newEnvironments.push(`${env}=${value}`) newEnvironments.push(`${env}=${value}`)
@@ -68,7 +73,7 @@ export async function startService(request: FastifyRequest<ServiceStartStop>) {
for (const secret of secrets) { for (const secret of secrets) {
const { name, value } = secret const { name, value } = secret
if (value) { 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}=`)) const foundNewEnv = !!newEnvironments?.find(env => env.startsWith(`${name}=`))
if (foundEnv && !foundNewEnv) { if (foundEnv && !foundNewEnv) {
newEnvironments.push(`${name}=${decrypt(value)}`) 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 } }) 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) { if (customVolumes.length > 0) {
for (const customVolume of customVolumes) { for (const customVolume of customVolumes) {
const { volumeName, path, containerId } = customVolume const { volumeName, path, containerId } = customVolume
@@ -85,41 +90,58 @@ export async function startService(request: FastifyRequest<ServiceStartStop>) {
} }
} }
} }
config[service] = { if (isWordpress) {
container_name: service, const { wordpress: { mysqlHost, mysqlPort, mysqlUser, mysqlPassword, mysqlDatabase, ownMysql } } = service
build: template.services[service].build || undefined, console.log({ mysqlHost, mysqlPort, mysqlUser, mysqlPassword, mysqlDatabase, ownMysql })
command: template.services[service].command, if (ownMysql) {
entrypoint: template.services[service]?.entrypoint, let envsToRemove = ['WORDPRESS_DB_HOST', 'WORDPRESS_DB_USER', 'WORDPRESS_DB_PASSWORD', 'WORDPRESS_DB_NAME']
image: arm ? template.services[service].imageArm : template.services[service].image, for (const env of newEnvironments) {
expose: template.services[service].ports, 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}`] } : {}), ...(exposePort ? { ports: [`${exposePort}:${exposePort}`] } : {}),
volumes, volumes,
environment: newEnvironments, environment: newEnvironments,
depends_on: template.services[service]?.depends_on, depends_on: template.services[s]?.depends_on,
ulimits: template.services[service]?.ulimits, ulimits: template.services[s]?.ulimits,
cap_drop: template.services[service]?.cap_drop, cap_drop: template.services[s]?.cap_drop,
cap_add: template.services[service]?.cap_add, cap_add: template.services[s]?.cap_add,
labels: makeLabelForServices(type), labels: makeLabelForServices(type),
...defaultComposeConfiguration(network), ...defaultComposeConfiguration(network),
} }
// Generate files for builds // Generate files for builds
if (template.services[service]?.files?.length > 0) { if (template.services[s]?.files?.length > 0) {
if (!template.services[service].build) { if (!template.services[s].build) {
template.services[service].build = { template.services[s].build = {
context: workdir, context: workdir,
dockerfile: `Dockerfile.${service}` dockerfile: `Dockerfile.${s}`
} }
} }
let Dockerfile = ` let Dockerfile = `
FROM ${template.services[service].image}` FROM ${template.services[s].image}`
for (const file of template.services[service].files) { for (const file of template.services[s].files) {
const { source, destination, content } = file; const { source, destination, content } = file;
await fs.writeFile(source, content); await fs.writeFile(source, content);
Dockerfile += ` Dockerfile += `
COPY ./${path.basename(source)} ${destination}` 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) 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) { export async function parseAndFindServiceTemplates(service: any, workdir?: string, isDeploy: boolean = false) {
const templates = await getTemplates() 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 = {} let parsedTemplate = {}
if (foundTemplate) { if (foundTemplate) {
if (!isDeploy) { if (!isDeploy) {
@@ -130,16 +130,21 @@ export async function parseAndFindServiceTemplates(service: any, workdir?: strin
for (const env of value.environment) { for (const env of value.environment) {
const [envKey, envValue] = env.split('=') const [envKey, envValue] = env.split('=')
const variable = foundTemplate.variables.find(v => v.name === envKey) || foundTemplate.variables.find(v => v.id === envValue) 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 label = variable?.label
const description = variable?.description const description = variable?.description
const defaultValue = variable?.defaultValue const defaultValue = variable?.defaultValue
const main = variable?.main || '$$id' 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')) { if (envValue.startsWith('$$config_coolify')) {
continue continue
} }
parsedTemplate[realKey].environment.push( 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) { if (value) {
strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, bcrypt.hashSync(value, 10) + "\"") strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, bcrypt.hashSync(value, 10) + "\"")
strParsedTemplate = strParsedTemplate.replaceAll(regex, value + "\"") 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 { id } = request.params;
const { type } = request.body; const { type } = request.body;
const templates = await getTemplates() 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) { if (foundTemplate) {
foundTemplate = JSON.parse(JSON.stringify(foundTemplate).replaceAll('$$id', id)) foundTemplate = JSON.parse(JSON.stringify(foundTemplate).replaceAll('$$id', id))
if (foundTemplate.variables.length > 0) { 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 } }) 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() return reply.code(201).send()
} else { } else {
throw { status: 404, message: 'Service type not found.' } 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 templates = await getTemplates()
const service = await prisma.service.findUnique({ where: { id } }) 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) { for (const setting of serviceSetting) {
let { id: settingId, name, value, changed = false, isNew = false, variableName } = setting let { id: settingId, name, value, changed = false, isNew = false, variableName } = setting
if (changed) { if (changed) {
@@ -506,11 +518,19 @@ export async function saveService(request: FastifyRequest<SaveService>, reply: F
export async function getServiceSecrets(request: FastifyRequest<OnlyId>) { export async function getServiceSecrets(request: FastifyRequest<OnlyId>) {
try { try {
const { id } = request.params const { id } = request.params
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
let secrets = await prisma.serviceSecret.findMany({ let secrets = await prisma.serviceSecret.findMany({
where: { serviceId: id }, where: { serviceId: id },
orderBy: { createdAt: 'desc' } orderBy: { createdAt: 'desc' }
}); });
const templates = await getTemplates()
const foundTemplate = templates.find(t => fixType(t.type) === service.type)
secrets = secrets.map((secret) => { 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); secret.value = decrypt(secret.value);
return secret; return secret;
}); });

View File

@@ -381,7 +381,7 @@ export async function traefikConfiguration(request, reply) {
} = service; } = service;
if (destinationDockerId) { if (destinationDockerId) {
const templates = await getTemplates(); const templates = await getTemplates();
let found = templates.find((a) => fixType(a.name) === fixType(type)); let found = templates.find((a) => a.type === type);
if (found) { if (found) {
found = JSON.parse(JSON.stringify(found).replaceAll('$$id', id)); found = JSON.parse(JSON.stringify(found).replaceAll('$$id', id));
for (const oneService of Object.keys(found.services)) { for (const oneService of Object.keys(found.services)) {
@@ -509,7 +509,7 @@ export async function traefikConfiguration(request, reply) {
} }
} else { } else {
traefik.http.routers[`${id}-${port || 'default'}`] = generateHttpRouter(`${id}-${port || 'default'}`, nakedDomain, pathPrefix) 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) traefik.http.services[`${id}-${port || 'default'}`] = generateLoadBalancerService(id, port)
if (!dualCerts) { if (!dualCerts) {
@@ -873,7 +873,7 @@ export async function remoteTraefikConfiguration(request: FastifyRequest<OnlyId>
} = service; } = service;
if (destinationDockerId) { if (destinationDockerId) {
const templates = await getTemplates(); const templates = await getTemplates();
let found = templates.find((a) => fixType(a.name) === fixType(type)); let found = templates.find((a) => a.type === type);
if (found) { if (found) {
const port = found.ports.main; const port = found.ports.main;
const publicPort = service[type]?.publicPort; 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 type: string;
export let isAbsolute = true; export let isAbsolute = true;
import * as Icons from '$lib/components/svg/services'; 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> </script>
<svelte:component this={Icons[name.replace('.','').replaceAll(' ','')]} {isAbsolute} /> <svelte:component this={Icons[name]} {isAbsolute} />

View File

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

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
export let name = ''; export let name = '';
export let value = ''; export let value = '';
export let readonly = false;
export let isNewSecret = false; export let isNewSecret = false;
import { page } from '$app/stores'; import { page } from '$app/stores';
@@ -55,19 +56,20 @@
bind:value={name} bind:value={name}
required required
placeholder="EXAMPLE_VARIABLE" placeholder="EXAMPLE_VARIABLE"
readonly={!isNewSecret} readonly={!isNewSecret || readonly}
class="w-full" class="w-full"
class:bg-coolblack={!isNewSecret} class:bg-coolblack={!isNewSecret}
class:border={!isNewSecret} class:border={!isNewSecret}
class:border-dashed={!isNewSecret} class:border-dashed={!isNewSecret}
class:border-coolgray-300={!isNewSecret} class:border-coolgray-300={!isNewSecret}
class:cursor-not-allowed={!isNewSecret}
/> />
</td> </td>
<td> <td>
<CopyPasswordField <CopyPasswordField
id={isNewSecret ? 'secretValue' : 'secretValueNew'} id={isNewSecret ? 'secretValue' : 'secretValueNew'}
name={isNewSecret ? 'secretValue' : 'secretValueNew'} name={isNewSecret ? 'secretValue' : 'secretValueNew'}
disabled={readonly}
{readonly}
isPasswordField={true} isPasswordField={true}
bind:value bind:value
placeholder="J$#@UIO%HO#$U%H" placeholder="J$#@UIO%HO#$U%H"
@@ -80,7 +82,7 @@
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<button class="btn btn-sm btn-primary" on:click={() => saveSecret(true)}>Add</button> <button class="btn btn-sm btn-primary" on:click={() => saveSecret(true)}>Add</button>
</div> </div>
{:else} {:else if !readonly}
<div class="flex flex-row justify-center space-x-2"> <div class="flex flex-row justify-center space-x-2">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<button class="btn btn-sm btn-primary" on:click={() => saveSecret(false)}>Set</button> <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) { async function handleSubmit(service: any) {
try { 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}`); return await goto(from || `/services/${id}`);
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
@@ -62,6 +62,7 @@
<div class="container lg:mx-auto lg:p-0 px-8 pt-5"> <div class="container lg:mx-auto lg:p-0 px-8 pt-5">
<div class="input-group flex w-full"> <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}> <div class="btn btn-square cursor-default no-animation hover:bg-error" on:click={cleanupSearch}>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -95,7 +96,7 @@
<div class="p-2"> <div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(service)}> <form on:submit|preventDefault={() => handleSubmit(service)}>
<button type="submit" class="box-selection relative text-xl font-bold hover:bg-primary"> <button type="submit" class="box-selection relative text-xl font-bold hover:bg-primary">
<ServiceIcons type={service.name} /> <ServiceIcons type={service.type} />
{service.name} {service.name}
</button> </button>
</form> </form>

View File

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

View File

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

View File

@@ -43,9 +43,16 @@ export async function saveSecret({
export async function saveForm(formData: any, service: any) { export async function saveForm(formData: any, service: any) {
const settings = service.serviceSetting.map((setting: { name: string }) => setting.name); 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']; const baseCoolifySetting = ['name', 'fqdn', 'exposePort', 'version'];
for (let field of formData) { for (let field of formData) {
const [key, value] = field; const [key, value] = field;
if (secrets.includes(key)) {
await post(`/services/${service.id}/secrets`, {
name: key,
value,
});
} else {
service.serviceSetting = service.serviceSetting.map((setting: any) => { service.serviceSetting = service.serviceSetting.map((setting: any) => {
if (setting.name === key) { if (setting.name === key) {
setting.changed = true; setting.changed = true;
@@ -65,6 +72,8 @@ export async function saveForm(formData: any, service: any) {
service[key] = value; service[key] = value;
} }
} }
}
await post(`/services/${service.id}`, { ...service }); await post(`/services/${service.id}`, { ...service });
const { service: reloadedService } = await get(`/services/${service.id}`); const { service: reloadedService } = await get(`/services/${service.id}`);
return reloadedService; return reloadedService;