From ba737c27f931954d3e3deae9a43f8d29034ee0f3 Mon Sep 17 00:00:00 2001 From: i325261 Date: Tue, 19 May 2026 13:22:36 +0200 Subject: [PATCH 1/9] [Enhancement] Improve template handling in CapOperatorBuildPlugin and add test for user-defined custom templates --- README.md | 4 +++ lib/build.js | 68 ++++++++++++++++++++++++++++++++-------------- test/build.test.js | 19 ++++++++++++- 3 files changed, 69 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 67734d1..03a21eb 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,10 @@ Depending on whether you used `--with-configurable-templates`, the design-time s > If you've already added the `templates` folder during the initial plugin call using `--with-templates` or `--with-configurable-templates` option, you can skip this step as the Helm chart is already complete. + > **Custom templates**: If you place files in the `chart/templates` folder, the build handles them as follows: + > - A file with the **same name** as a plugin-generated template (e.g. `_helpers.tpl`, `domain.yaml`, `cap-operator-cros.yaml`, `service-binding.yaml`, `service-instance.yaml`) will be used as-is instead of the plugin's default, and a message is printed to indicate this. + > - Any **additional files** you add to `chart/templates` are copied alongside the standard templates without modification. + 2. Up to this point, you've only filled in the design time information in the chart. But to deploy the application, you need to create a `runtime-values.yaml` file with all the runtime values, as mentioned in the section on configuration. You can generate the file using the plugin itself. The plugin requires the following information to generate the `runtime-values.yaml`: diff --git a/lib/build.js b/lib/build.js index 4643af3..1ab9338 100644 --- a/lib/build.js +++ b/lib/build.js @@ -5,6 +5,7 @@ SPDX-License-Identifier: Apache-2.0 const cds = require('@sap/cds-dk') const yaml = require('@sap/cds-foss').yaml +const fs = require('fs') const { exists, path } = cds.utils const { isServiceOnlyChart, @@ -33,32 +34,57 @@ module.exports = class CapOperatorBuildPlugin extends cds.build.Plugin { } async copyTemplates() { - if (exists(path.join(this.task.src, 'chart/templates'))) { - return await this.copy(path.join(this.task.src, 'chart/templates')).to(path.join(this.task.dest, 'templates')) + const userTemplatesDir = path.join(this.task.src, 'chart/templates') + const destTemplatesDir = path.join(this.task.dest, 'templates') + const hasUserTemplates = exists(userTemplatesDir) + const customTemplateMsg = (name) => `[cap-operator-plugin] Using template '${name}' from chart/templates/` + + for (const name of ['service-binding.yaml', 'service-instance.yaml']) { + const userFile = path.join(userTemplatesDir, name) + if (hasUserTemplates && exists(userFile)) { + console.log(customTemplateMsg(name)) + await this.copy(userFile).to(path.join(destTemplatesDir, name)) + } else { + await this.copy(path.join(__dirname, `../files/commonTemplates/${name}`)).to(path.join(destTemplatesDir, name)) + } } - await this.copy(path.join(__dirname, '../files/commonTemplates/')).to(path.join(this.task.dest, 'templates/')) - const valuesYaml = yaml.parse(await cds.utils.read(path.join(this.task.src, 'chart/values.yaml'))) - - // Create _helpers.tpl - await cds.utils.write(getHelperTpl({ - hasXsuaa: getServiceInstanceKeyName(valuesYaml['serviceInstances'], 'xsuaa') != null - })).to(path.join(this.task.dest, 'templates/_helpers.tpl')) - const hasIas = getServiceInstanceKeyName(valuesYaml['serviceInstances'], 'identity') != null - // Create domain.yaml - await cds.utils.write(getDomainCroYaml({ - hasIas: hasIas - })).to(path.join(this.task.dest, 'templates/domain.yaml')) - - // Create cap-operator-cros.yaml - // Only filling those fields in the project input struct that are required to create CAPApplication - await cds.utils.write(getCAPOpCroYaml({ - hasIas: hasIas, - isService: isServiceOnlyChart('chart') - })).to(path.join(this.task.dest, 'templates/cap-operator-cros.yaml')) + const generatedFiles = [ + { + name: '_helpers.tpl', + generate: () => getHelperTpl({ hasXsuaa: getServiceInstanceKeyName(valuesYaml['serviceInstances'], 'xsuaa') != null }) + }, + { + name: 'domain.yaml', + generate: () => getDomainCroYaml({ hasIas }) + }, + { + name: 'cap-operator-cros.yaml', + generate: () => getCAPOpCroYaml({ hasIas, isService: isServiceOnlyChart('chart') }) + } + ] + + for (const { name, generate } of generatedFiles) { + const userFile = path.join(userTemplatesDir, name) + if (hasUserTemplates && exists(userFile)) { + console.log(customTemplateMsg(name)) + await this.copy(userFile).to(path.join(destTemplatesDir, name)) + } else { + await cds.utils.write(generate()).to(path.join(destTemplatesDir, name)) + } + } + + if (hasUserTemplates) { + const knownFiles = new Set(['service-binding.yaml', 'service-instance.yaml', '_helpers.tpl', 'domain.yaml', 'cap-operator-cros.yaml']) + for (const entry of await fs.promises.readdir(userTemplatesDir)) { + if (!knownFiles.has(entry)) { + await this.copy(path.join(userTemplatesDir, entry)).to(path.join(destTemplatesDir, entry)) + } + } + } } async copyChartYaml() { diff --git a/test/build.test.js b/test/build.test.js index 920fca4..a43a4fd 100644 --- a/test/build.test.js +++ b/test/build.test.js @@ -47,6 +47,23 @@ describe('cds build', () => { }) + it('Build cap-operator chart with user-defined custom template', async () => { + execSync(`cds add cap-operator`, { cwd: bookshop }) + + fs.mkdirSync(join(bookshop, 'chart/templates'), { recursive: true }) + fs.writeFileSync(join(bookshop, 'chart/templates/my-custom-template.yaml'), '# custom user template\n') + + execSync(`cds build`, { cwd: bookshop }) + + expect(fs.readFileSync(join(bookshop, 'gen/chart/templates/my-custom-template.yaml'), 'utf8')).to.equal('# custom user template\n') + + expect(fs.existsSync(join(bookshop, 'gen/chart/templates/_helpers.tpl'))).to.equal(true) + expect(fs.existsSync(join(bookshop, 'gen/chart/templates/domain.yaml'))).to.equal(true) + expect(fs.existsSync(join(bookshop, 'gen/chart/templates/cap-operator-cros.yaml'))).to.equal(true) + expect(fs.existsSync(join(bookshop, 'gen/chart/templates/service-binding.yaml'))).to.equal(true) + expect(fs.existsSync(join(bookshop, 'gen/chart/templates/service-instance.yaml'))).to.equal(true) + }) + it('Build cap-operator chart with modified templates', async () => { execSync(`cds add cap-operator --with-templates`, { cwd: bookshop }) @@ -64,6 +81,6 @@ describe('cds build', () => { expect(getFileHash(join(__dirname,'../files/commonTemplates/service-instance.yaml'))).to.equal(getFileHash(join(bookshop, 'gen/chart/templates/service-instance.yaml'))) expect(getFileHash(join(__dirname,'files/domain.yaml'))).to.equal(getFileHash(join(bookshop, 'gen/chart/templates/domain.yaml'))) - expect(fs.existsSync(join(bookshop, 'gen/chart/templates/_helpers.tpl'))).to.equal(false) + expect(fs.existsSync(join(bookshop, 'gen/chart/templates/_helpers.tpl'))).to.equal(true) }) }) From 869e1ec502010bd34771ae5802a989f8155c0793 Mon Sep 17 00:00:00 2001 From: i325261 Date: Tue, 19 May 2026 13:44:20 +0200 Subject: [PATCH 2/9] [Enhancement] Add logging for template usage and Helm chart generation in CapOperatorBuildPlugin --- lib/build.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/build.js b/lib/build.js index 1ab9338..051370f 100644 --- a/lib/build.js +++ b/lib/build.js @@ -38,6 +38,7 @@ module.exports = class CapOperatorBuildPlugin extends cds.build.Plugin { const destTemplatesDir = path.join(this.task.dest, 'templates') const hasUserTemplates = exists(userTemplatesDir) const customTemplateMsg = (name) => `[cap-operator-plugin] Using template '${name}' from chart/templates/` + const defaultTemplateMsg = (name) => `[cap-operator-plugin] Using default template for '${name}'` for (const name of ['service-binding.yaml', 'service-instance.yaml']) { const userFile = path.join(userTemplatesDir, name) @@ -45,6 +46,7 @@ module.exports = class CapOperatorBuildPlugin extends cds.build.Plugin { console.log(customTemplateMsg(name)) await this.copy(userFile).to(path.join(destTemplatesDir, name)) } else { + console.log(defaultTemplateMsg(name)) await this.copy(path.join(__dirname, `../files/commonTemplates/${name}`)).to(path.join(destTemplatesDir, name)) } } @@ -73,6 +75,7 @@ module.exports = class CapOperatorBuildPlugin extends cds.build.Plugin { console.log(customTemplateMsg(name)) await this.copy(userFile).to(path.join(destTemplatesDir, name)) } else { + console.log(defaultTemplateMsg(name)) await cds.utils.write(generate()).to(path.join(destTemplatesDir, name)) } } @@ -81,6 +84,7 @@ module.exports = class CapOperatorBuildPlugin extends cds.build.Plugin { const knownFiles = new Set(['service-binding.yaml', 'service-instance.yaml', '_helpers.tpl', 'domain.yaml', 'cap-operator-cros.yaml']) for (const entry of await fs.promises.readdir(userTemplatesDir)) { if (!knownFiles.has(entry)) { + console.log(`[cap-operator-plugin] Copying user defined template '${entry}' from chart/templates/`) await this.copy(path.join(userTemplatesDir, entry)).to(path.join(destTemplatesDir, entry)) } } @@ -101,6 +105,8 @@ module.exports = class CapOperatorBuildPlugin extends cds.build.Plugin { } async build() { + console.log(`[cap-operator-plugin] Generating Helm chart in ${this.task.dest}...`) + // Copy templates await this.copyTemplates() From 648619d879e8d90ddb7c41012cee7b4d8d06595e Mon Sep 17 00:00:00 2001 From: i325261 Date: Thu, 21 May 2026 13:26:37 +0200 Subject: [PATCH 3/9] [Enhancement] Refactor template handling in CapOperatorBuildPlugin to improve user-defined template support and logging --- lib/build.js | 53 ++++++++++++++++++++++++---------------------------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/lib/build.js b/lib/build.js index 051370f..2463e32 100644 --- a/lib/build.js +++ b/lib/build.js @@ -40,43 +40,38 @@ module.exports = class CapOperatorBuildPlugin extends cds.build.Plugin { const customTemplateMsg = (name) => `[cap-operator-plugin] Using template '${name}' from chart/templates/` const defaultTemplateMsg = (name) => `[cap-operator-plugin] Using default template for '${name}'` - for (const name of ['service-binding.yaml', 'service-instance.yaml']) { - const userFile = path.join(userTemplatesDir, name) - if (hasUserTemplates && exists(userFile)) { - console.log(customTemplateMsg(name)) - await this.copy(userFile).to(path.join(destTemplatesDir, name)) - } else { - console.log(defaultTemplateMsg(name)) - await this.copy(path.join(__dirname, `../files/commonTemplates/${name}`)).to(path.join(destTemplatesDir, name)) - } + const staticEntry = (name) => { + const defaultFile = path.join(__dirname, `../files/commonTemplates/${name}`) + return { name, getDefault: () => cds.utils.read(defaultFile), writeDefault: (dest) => this.copy(defaultFile).to(dest) } } + const generatedEntry = (name, generate) => ({ + name, getDefault: () => generate(), writeDefault: (dest) => cds.utils.write(generate()).to(dest) + }) const valuesYaml = yaml.parse(await cds.utils.read(path.join(this.task.src, 'chart/values.yaml'))) const hasIas = getServiceInstanceKeyName(valuesYaml['serviceInstances'], 'identity') != null - const generatedFiles = [ - { - name: '_helpers.tpl', - generate: () => getHelperTpl({ hasXsuaa: getServiceInstanceKeyName(valuesYaml['serviceInstances'], 'xsuaa') != null }) - }, - { - name: 'domain.yaml', - generate: () => getDomainCroYaml({ hasIas }) - }, - { - name: 'cap-operator-cros.yaml', - generate: () => getCAPOpCroYaml({ hasIas, isService: isServiceOnlyChart('chart') }) - } + const templates = [ + staticEntry('service-binding.yaml'), + staticEntry('service-instance.yaml'), + generatedEntry('_helpers.tpl', () => getHelperTpl({ hasXsuaa: getServiceInstanceKeyName(valuesYaml['serviceInstances'], 'xsuaa') != null })), + generatedEntry('domain.yaml', () => getDomainCroYaml({ hasIas })), + generatedEntry('cap-operator-cros.yaml', () => getCAPOpCroYaml({ hasIas, isService: isServiceOnlyChart('chart') })) ] - for (const { name, generate } of generatedFiles) { + for (const { name, getDefault, writeDefault } of templates) { const userFile = path.join(userTemplatesDir, name) + const destFile = path.join(destTemplatesDir, name) if (hasUserTemplates && exists(userFile)) { - console.log(customTemplateMsg(name)) - await this.copy(userFile).to(path.join(destTemplatesDir, name)) + const [userContent, defaultContent] = await Promise.all([cds.utils.read(userFile), Promise.resolve(getDefault())]) + this.pushMessage( + userContent !== defaultContent ? customTemplateMsg(name) : defaultTemplateMsg(name), + userContent !== defaultContent ? CapOperatorBuildPlugin.WARNING : CapOperatorBuildPlugin.INFO + ) + await this.copy(userFile).to(destFile) } else { - console.log(defaultTemplateMsg(name)) - await cds.utils.write(generate()).to(path.join(destTemplatesDir, name)) + this.pushMessage(defaultTemplateMsg(name), CapOperatorBuildPlugin.INFO) + await writeDefault(destFile) } } @@ -84,7 +79,7 @@ module.exports = class CapOperatorBuildPlugin extends cds.build.Plugin { const knownFiles = new Set(['service-binding.yaml', 'service-instance.yaml', '_helpers.tpl', 'domain.yaml', 'cap-operator-cros.yaml']) for (const entry of await fs.promises.readdir(userTemplatesDir)) { if (!knownFiles.has(entry)) { - console.log(`[cap-operator-plugin] Copying user defined template '${entry}' from chart/templates/`) + this.pushMessage(`[cap-operator-plugin] Copying user defined template '${entry}' from chart/templates/`, CapOperatorBuildPlugin.INFO) await this.copy(path.join(userTemplatesDir, entry)).to(path.join(destTemplatesDir, entry)) } } @@ -105,7 +100,7 @@ module.exports = class CapOperatorBuildPlugin extends cds.build.Plugin { } async build() { - console.log(`[cap-operator-plugin] Generating Helm chart in ${this.task.dest}...`) + this.pushMessage(`[cap-operator-plugin] Generating Helm chart in ${this.task.dest}...`, CapOperatorBuildPlugin.INFO) // Copy templates await this.copyTemplates() From e60d483b8ed839323645dbe25cae0249cf7c070d Mon Sep 17 00:00:00 2001 From: i325261 Date: Thu, 21 May 2026 13:38:06 +0200 Subject: [PATCH 4/9] [Enhancement] Update logging message for user-defined templates in CapOperatorBuildPlugin --- lib/build.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/build.js b/lib/build.js index 2463e32..698e452 100644 --- a/lib/build.js +++ b/lib/build.js @@ -37,7 +37,7 @@ module.exports = class CapOperatorBuildPlugin extends cds.build.Plugin { const userTemplatesDir = path.join(this.task.src, 'chart/templates') const destTemplatesDir = path.join(this.task.dest, 'templates') const hasUserTemplates = exists(userTemplatesDir) - const customTemplateMsg = (name) => `[cap-operator-plugin] Using template '${name}' from chart/templates/` + const customTemplateMsg = (name) => `[cap-operator-plugin] Using updated template '${name}' from chart/templates/` const defaultTemplateMsg = (name) => `[cap-operator-plugin] Using default template for '${name}'` const staticEntry = (name) => { From 0361223ade906678d6aa74a65f7a9e4dc91ce15e Mon Sep 17 00:00:00 2001 From: i325261 Date: Thu, 21 May 2026 14:50:03 +0200 Subject: [PATCH 5/9] [Enhancement] Integrate configurable template support in CapOperatorBuildPlugin and update template handling logic --- lib/build.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/build.js b/lib/build.js index 698e452..dba468b 100644 --- a/lib/build.js +++ b/lib/build.js @@ -10,9 +10,11 @@ const { exists, path } = cds.utils const { isServiceOnlyChart, getCAPOpCroYaml, + getConfigurableCapOpCroYaml, getServiceInstanceKeyName, getDomainCroYaml, - getHelperTpl + getHelperTpl, + isConfigurableTemplateChart } = require('./util') module.exports = class CapOperatorBuildPlugin extends cds.build.Plugin { @@ -37,6 +39,7 @@ module.exports = class CapOperatorBuildPlugin extends cds.build.Plugin { const userTemplatesDir = path.join(this.task.src, 'chart/templates') const destTemplatesDir = path.join(this.task.dest, 'templates') const hasUserTemplates = exists(userTemplatesDir) + const isConfigurableTempChart = isConfigurableTemplateChart(path.join(this.task.src, 'chart')) const customTemplateMsg = (name) => `[cap-operator-plugin] Using updated template '${name}' from chart/templates/` const defaultTemplateMsg = (name) => `[cap-operator-plugin] Using default template for '${name}'` @@ -56,14 +59,14 @@ module.exports = class CapOperatorBuildPlugin extends cds.build.Plugin { staticEntry('service-instance.yaml'), generatedEntry('_helpers.tpl', () => getHelperTpl({ hasXsuaa: getServiceInstanceKeyName(valuesYaml['serviceInstances'], 'xsuaa') != null })), generatedEntry('domain.yaml', () => getDomainCroYaml({ hasIas })), - generatedEntry('cap-operator-cros.yaml', () => getCAPOpCroYaml({ hasIas, isService: isServiceOnlyChart('chart') })) + generatedEntry('cap-operator-cros.yaml', () => isConfigurableTempChart ? getConfigurableCapOpCroYaml({ hasIas, isService: isServiceOnlyChart('chart') }) : getCAPOpCroYaml({ hasIas, isService: isServiceOnlyChart('chart') })) ] for (const { name, getDefault, writeDefault } of templates) { const userFile = path.join(userTemplatesDir, name) const destFile = path.join(destTemplatesDir, name) if (hasUserTemplates && exists(userFile)) { - const [userContent, defaultContent] = await Promise.all([cds.utils.read(userFile), Promise.resolve(getDefault())]) + const [userContent, defaultContent] = await Promise.all([cds.utils.read(userFile), getDefault()]) this.pushMessage( userContent !== defaultContent ? customTemplateMsg(name) : defaultTemplateMsg(name), userContent !== defaultContent ? CapOperatorBuildPlugin.WARNING : CapOperatorBuildPlugin.INFO From a8ae8526d5e4f8862875c2ff7d9336903da4c1c6 Mon Sep 17 00:00:00 2001 From: Anirudh Prasad Date: Thu, 21 May 2026 14:57:38 +0200 Subject: [PATCH 6/9] Update lib/build.js Co-authored-by: hyperspace-insights[bot] <209611008+hyperspace-insights[bot]@users.noreply.github.com> --- lib/build.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/build.js b/lib/build.js index dba468b..608335b 100644 --- a/lib/build.js +++ b/lib/build.js @@ -57,7 +57,7 @@ module.exports = class CapOperatorBuildPlugin extends cds.build.Plugin { const templates = [ staticEntry('service-binding.yaml'), staticEntry('service-instance.yaml'), - generatedEntry('_helpers.tpl', () => getHelperTpl({ hasXsuaa: getServiceInstanceKeyName(valuesYaml['serviceInstances'], 'xsuaa') != null })), + generatedEntry('_helpers.tpl', () => getHelperTpl({ hasXsuaa: getServiceInstanceKeyName(valuesYaml['serviceInstances'], 'xsuaa') != null }, isConfigurableTempChart)), generatedEntry('domain.yaml', () => getDomainCroYaml({ hasIas })), generatedEntry('cap-operator-cros.yaml', () => isConfigurableTempChart ? getConfigurableCapOpCroYaml({ hasIas, isService: isServiceOnlyChart('chart') }) : getCAPOpCroYaml({ hasIas, isService: isServiceOnlyChart('chart') })) ] From e447aaf0e2cb88ce72fccaf025ccfb5e605bb08c Mon Sep 17 00:00:00 2001 From: i325261 Date: Thu, 21 May 2026 16:02:19 +0200 Subject: [PATCH 7/9] [Enhancement] Improve handling of user-defined templates in CapOperatorBuildPlugin and update tests for nested templates --- README.md | 4 ++-- lib/build.js | 14 ++++++++------ test/build.test.js | 5 ++++- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 03a21eb..ce0d17d 100644 --- a/README.md +++ b/README.md @@ -162,8 +162,8 @@ Depending on whether you used `--with-configurable-templates`, the design-time s > If you've already added the `templates` folder during the initial plugin call using `--with-templates` or `--with-configurable-templates` option, you can skip this step as the Helm chart is already complete. > **Custom templates**: If you place files in the `chart/templates` folder, the build handles them as follows: - > - A file with the **same name** as a plugin-generated template (e.g. `_helpers.tpl`, `domain.yaml`, `cap-operator-cros.yaml`, `service-binding.yaml`, `service-instance.yaml`) will be used as-is instead of the plugin's default, and a message is printed to indicate this. - > - Any **additional files** you add to `chart/templates` are copied alongside the standard templates without modification. + > - A file with the **same name** as one of the plugin-generated templates (`_helpers.tpl`, `domain.yaml`, `cap-operator-cros.yaml`, `service-binding.yaml`, `service-instance.yaml`) will be used as-is instead of the plugin's default, and a message is printed to indicate this. + > - Any **additional files** you add to `chart/templates` (i.e. files with names not in the list above) are copied alongside the standard templates without modification. 2. Up to this point, you've only filled in the design time information in the chart. But to deploy the application, you need to create a `runtime-values.yaml` file with all the runtime values, as mentioned in the section on configuration. You can generate the file using the plugin itself. diff --git a/lib/build.js b/lib/build.js index 608335b..0ad1f43 100644 --- a/lib/build.js +++ b/lib/build.js @@ -67,9 +67,11 @@ module.exports = class CapOperatorBuildPlugin extends cds.build.Plugin { const destFile = path.join(destTemplatesDir, name) if (hasUserTemplates && exists(userFile)) { const [userContent, defaultContent] = await Promise.all([cds.utils.read(userFile), getDefault()]) + const userStr = userContent?.toString() + const defaultStr = defaultContent?.toString() this.pushMessage( - userContent !== defaultContent ? customTemplateMsg(name) : defaultTemplateMsg(name), - userContent !== defaultContent ? CapOperatorBuildPlugin.WARNING : CapOperatorBuildPlugin.INFO + userStr !== defaultStr ? customTemplateMsg(name) : defaultTemplateMsg(name), + userStr !== defaultStr ? CapOperatorBuildPlugin.WARNING : CapOperatorBuildPlugin.INFO ) await this.copy(userFile).to(destFile) } else { @@ -80,10 +82,10 @@ module.exports = class CapOperatorBuildPlugin extends cds.build.Plugin { if (hasUserTemplates) { const knownFiles = new Set(['service-binding.yaml', 'service-instance.yaml', '_helpers.tpl', 'domain.yaml', 'cap-operator-cros.yaml']) - for (const entry of await fs.promises.readdir(userTemplatesDir)) { - if (!knownFiles.has(entry)) { - this.pushMessage(`[cap-operator-plugin] Copying user defined template '${entry}' from chart/templates/`, CapOperatorBuildPlugin.INFO) - await this.copy(path.join(userTemplatesDir, entry)).to(path.join(destTemplatesDir, entry)) + for (const entry of await fs.promises.readdir(userTemplatesDir, { withFileTypes: true })) { + if (entry.isDirectory() || !knownFiles.has(entry.name)) { + this.pushMessage(`[cap-operator-plugin] Copying user defined template '${entry.name}' from chart/templates/`, CapOperatorBuildPlugin.INFO) + await this.copy(path.join(userTemplatesDir, entry.name)).to(path.join(destTemplatesDir, entry.name)) } } } diff --git a/test/build.test.js b/test/build.test.js index a43a4fd..8bef76d 100644 --- a/test/build.test.js +++ b/test/build.test.js @@ -48,14 +48,17 @@ describe('cds build', () => { }) it('Build cap-operator chart with user-defined custom template', async () => { + execSync(`rm -rf gen`, { cwd: bookshop }) execSync(`cds add cap-operator`, { cwd: bookshop }) - fs.mkdirSync(join(bookshop, 'chart/templates'), { recursive: true }) + fs.mkdirSync(join(bookshop, 'chart/templates/my-subdir'), { recursive: true }) fs.writeFileSync(join(bookshop, 'chart/templates/my-custom-template.yaml'), '# custom user template\n') + fs.writeFileSync(join(bookshop, 'chart/templates/my-subdir/my-nested-template.yaml'), '# custom nested template\n') execSync(`cds build`, { cwd: bookshop }) expect(fs.readFileSync(join(bookshop, 'gen/chart/templates/my-custom-template.yaml'), 'utf8')).to.equal('# custom user template\n') + expect(fs.readFileSync(join(bookshop, 'gen/chart/templates/my-subdir/my-nested-template.yaml'), 'utf8')).to.equal('# custom nested template\n') expect(fs.existsSync(join(bookshop, 'gen/chart/templates/_helpers.tpl'))).to.equal(true) expect(fs.existsSync(join(bookshop, 'gen/chart/templates/domain.yaml'))).to.equal(true) From 55d93509d9b5754487c7a3526a74f7357b7dd014 Mon Sep 17 00:00:00 2001 From: i325261 Date: Tue, 26 May 2026 09:49:14 +0200 Subject: [PATCH 8/9] getProject values util --- lib/build.js | 13 ++++++------- lib/util.js | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/lib/build.js b/lib/build.js index 0ad1f43..ecacef4 100644 --- a/lib/build.js +++ b/lib/build.js @@ -4,14 +4,13 @@ SPDX-License-Identifier: Apache-2.0 */ const cds = require('@sap/cds-dk') -const yaml = require('@sap/cds-foss').yaml const fs = require('fs') const { exists, path } = cds.utils const { isServiceOnlyChart, getCAPOpCroYaml, getConfigurableCapOpCroYaml, - getServiceInstanceKeyName, + getProjectFromValuesYaml, getDomainCroYaml, getHelperTpl, isConfigurableTemplateChart @@ -51,15 +50,15 @@ module.exports = class CapOperatorBuildPlugin extends cds.build.Plugin { name, getDefault: () => generate(), writeDefault: (dest) => cds.utils.write(generate()).to(dest) }) - const valuesYaml = yaml.parse(await cds.utils.read(path.join(this.task.src, 'chart/values.yaml'))) - const hasIas = getServiceInstanceKeyName(valuesYaml['serviceInstances'], 'identity') != null + const isService = isServiceOnlyChart('chart') + const project = await getProjectFromValuesYaml(path.join(this.task.src, 'chart'), isService) const templates = [ staticEntry('service-binding.yaml'), staticEntry('service-instance.yaml'), - generatedEntry('_helpers.tpl', () => getHelperTpl({ hasXsuaa: getServiceInstanceKeyName(valuesYaml['serviceInstances'], 'xsuaa') != null }, isConfigurableTempChart)), - generatedEntry('domain.yaml', () => getDomainCroYaml({ hasIas })), - generatedEntry('cap-operator-cros.yaml', () => isConfigurableTempChart ? getConfigurableCapOpCroYaml({ hasIas, isService: isServiceOnlyChart('chart') }) : getCAPOpCroYaml({ hasIas, isService: isServiceOnlyChart('chart') })) + generatedEntry('_helpers.tpl', () => getHelperTpl(project, isConfigurableTempChart)), + generatedEntry('domain.yaml', () => getDomainCroYaml(project)), + generatedEntry('cap-operator-cros.yaml', () => isConfigurableTempChart ? getConfigurableCapOpCroYaml(project) : getCAPOpCroYaml(project)) ] for (const { name, getDefault, writeDefault } of templates) { diff --git a/lib/util.js b/lib/util.js index 0ed7cd6..d614ab7 100644 --- a/lib/util.js +++ b/lib/util.js @@ -216,6 +216,24 @@ function getServiceInstanceKeyName(serviceInstances, offeringName) { ) ?? null } +async function getProjectFromValuesYaml(chartPath, isService) { + const valuesYaml = yaml.parse(await cds.utils.read(cds.utils.path.join(chartPath, 'values.yaml'))) + const chartYaml = yaml.parse(await cds.utils.read(cds.utils.path.join(chartPath, 'Chart.yaml'))) + const si = valuesYaml['serviceInstances'] + const has = offering => getServiceInstanceKeyName(si, offering) != null + return { + isService, + appName: chartYaml.name, + hasXsuaa: has('xsuaa'), + hasIas: has('identity'), + hasDestination: has('destination'), + hasHtml5Repo: has('html5-apps-repo'), + hasMultitenancy: has('saas-registry') || has('subscription-manager'), + hasApprouter: valuesYaml.workloads?.appRouter != null, + hasAms: valuesYaml.workloads?.amsDeployer != null, + } +} + function yamlBuilder() { const lines = [] let indent = 0 @@ -739,6 +757,7 @@ module.exports = { transformValuesAndFillCapOpCroYaml, convertHypenNameToCamelcase, getServiceInstanceKeyName, + getProjectFromValuesYaml, getConfigurableCapOpCroYaml, getCAPOpCroYaml, getDomainCroYaml, From 51c0e1cd884d21bbe7cfd413bab648a96b1c0cbc Mon Sep 17 00:00:00 2001 From: i325261 Date: Tue, 26 May 2026 11:11:36 +0200 Subject: [PATCH 9/9] [Refactor] Simplify getProjectFromValuesYaml function by removing isService parameter and determining service status internally --- lib/build.js | 4 +--- lib/util.js | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/build.js b/lib/build.js index ecacef4..3ac731b 100644 --- a/lib/build.js +++ b/lib/build.js @@ -7,7 +7,6 @@ const cds = require('@sap/cds-dk') const fs = require('fs') const { exists, path } = cds.utils const { - isServiceOnlyChart, getCAPOpCroYaml, getConfigurableCapOpCroYaml, getProjectFromValuesYaml, @@ -50,8 +49,7 @@ module.exports = class CapOperatorBuildPlugin extends cds.build.Plugin { name, getDefault: () => generate(), writeDefault: (dest) => cds.utils.write(generate()).to(dest) }) - const isService = isServiceOnlyChart('chart') - const project = await getProjectFromValuesYaml(path.join(this.task.src, 'chart'), isService) + const project = await getProjectFromValuesYaml(path.join(this.task.src, 'chart')) const templates = [ staticEntry('service-binding.yaml'), diff --git a/lib/util.js b/lib/util.js index d614ab7..8c9b213 100644 --- a/lib/util.js +++ b/lib/util.js @@ -216,13 +216,13 @@ function getServiceInstanceKeyName(serviceInstances, offeringName) { ) ?? null } -async function getProjectFromValuesYaml(chartPath, isService) { +async function getProjectFromValuesYaml(chartPath) { const valuesYaml = yaml.parse(await cds.utils.read(cds.utils.path.join(chartPath, 'values.yaml'))) const chartYaml = yaml.parse(await cds.utils.read(cds.utils.path.join(chartPath, 'Chart.yaml'))) const si = valuesYaml['serviceInstances'] const has = offering => getServiceInstanceKeyName(si, offering) != null return { - isService, + isService: chartYaml.annotations?.["app.kubernetes.io/component"] === 'service-only' || false, appName: chartYaml.name, hasXsuaa: has('xsuaa'), hasIas: has('identity'),