diff --git a/README.md b/README.md index 67734d1..ce0d17d 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 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. The plugin requires the following information to generate the `runtime-values.yaml`: diff --git a/lib/build.js b/lib/build.js index 4643af3..0ad1f43 100644 --- a/lib/build.js +++ b/lib/build.js @@ -5,13 +5,16 @@ 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, getDomainCroYaml, - getHelperTpl + getHelperTpl, + isConfigurableTemplateChart } = require('./util') module.exports = class CapOperatorBuildPlugin extends cds.build.Plugin { @@ -33,32 +36,59 @@ 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 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}'` + + 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) } } - - await this.copy(path.join(__dirname, '../files/commonTemplates/')).to(path.join(this.task.dest, 'templates/')) + 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'))) - - // 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 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') })) + ] + + 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), getDefault()]) + const userStr = userContent?.toString() + const defaultStr = defaultContent?.toString() + this.pushMessage( + userStr !== defaultStr ? customTemplateMsg(name) : defaultTemplateMsg(name), + userStr !== defaultStr ? CapOperatorBuildPlugin.WARNING : CapOperatorBuildPlugin.INFO + ) + await this.copy(userFile).to(destFile) + } else { + this.pushMessage(defaultTemplateMsg(name), CapOperatorBuildPlugin.INFO) + await writeDefault(destFile) + } + } + + 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, { 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)) + } + } + } } async copyChartYaml() { @@ -75,6 +105,8 @@ module.exports = class CapOperatorBuildPlugin extends cds.build.Plugin { } async build() { + this.pushMessage(`[cap-operator-plugin] Generating Helm chart in ${this.task.dest}...`, CapOperatorBuildPlugin.INFO) + // Copy templates await this.copyTemplates() diff --git a/test/build.test.js b/test/build.test.js index 920fca4..8bef76d 100644 --- a/test/build.test.js +++ b/test/build.test.js @@ -47,6 +47,26 @@ 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/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) + 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 +84,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) }) })