Skip to content
Draft
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand Down
76 changes: 54 additions & 22 deletions lib/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Comment thread
anirudhprasad-sap marked this conversation as resolved.
})

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() {
Expand All @@ -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()

Expand Down
22 changes: 21 additions & 1 deletion test/build.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Comment thread
anirudhprasad-sap marked this conversation as resolved.

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 })

Expand All @@ -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)
})
})
Loading