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..3ac731b 100644 --- a/lib/build.js +++ b/lib/build.js @@ -4,14 +4,15 @@ 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, - getServiceInstanceKeyName, + getConfigurableCapOpCroYaml, + getProjectFromValuesYaml, getDomainCroYaml, - getHelperTpl + getHelperTpl, + isConfigurableTemplateChart } = require('./util') module.exports = class CapOperatorBuildPlugin extends cds.build.Plugin { @@ -33,32 +34,58 @@ 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) } + } + const generatedEntry = (name, generate) => ({ + name, getDefault: () => generate(), writeDefault: (dest) => cds.utils.write(generate()).to(dest) + }) + + const project = await getProjectFromValuesYaml(path.join(this.task.src, 'chart')) + + const templates = [ + staticEntry('service-binding.yaml'), + staticEntry('service-instance.yaml'), + 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) { + 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) + } } - 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')) + 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 +102,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/lib/util.js b/lib/util.js index 0ed7cd6..8c9b213 100644 --- a/lib/util.js +++ b/lib/util.js @@ -216,6 +216,24 @@ function getServiceInstanceKeyName(serviceInstances, offeringName) { ) ?? null } +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: chartYaml.annotations?.["app.kubernetes.io/component"] === 'service-only' || false, + 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, 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) }) })