Skip to content

Commit a59131a

Browse files
[Feat] Support for IAS & AMS based subscription (#75)
1 parent 5030399 commit a59131a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+2874
-894
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). The format is based on [Keep a Changelog](http://keepachangelog.com/).
44

5+
## Version 0.10.0 - 03-November-2025
6+
7+
### Added
8+
9+
- Added support for IAS and AMS based subscriptions.
10+
- Template generation refactored to use common template functions for better maintainability.
11+
512
## Version 0.9.0 - 08-September-2025
613

714
### Fixed

bin/cap-op-plugin.js

Lines changed: 60 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,18 @@ const yaml = require('@sap/cds-foss').yaml
1111
const Mustache = require('mustache')
1212
const { spawn } = require('child_process')
1313

14-
const { ask, mergeObj, isCAPOperatorChart, isConfigurableTemplateChart, transformValuesAndFillCapOpCroYaml, isServiceOnlyChart } = require('../lib/util')
14+
const {
15+
ask,
16+
mergeObj,
17+
isCAPOperatorChart,
18+
isConfigurableTemplateChart,
19+
transformValuesAndFillCapOpCroYaml,
20+
isServiceOnlyChart,
21+
getServiceInstanceKeyName,
22+
getConfigurableCapOpCroYaml,
23+
getDomainCroYaml,
24+
getHelperTpl
25+
} = require('../lib/util')
1526

1627
const SUPPORTED = { 'generate-runtime-values': ['--with-input-yaml'], 'convert-to-configurable-template-chart': ['--with-runtime-yaml'] }
1728

@@ -25,7 +36,7 @@ async function capOperatorPlugin(cmd, option, yamlPath) {
2536
if (option === '--with-input-yaml' && !yamlPath)
2637
return _usage(`Input yaml path is missing.`)
2738

28-
if (option === '--with-input-yaml' && !yamlPath && cds.utils.exists(cds.utils.path.join(cds.root,yamlPath)))
39+
if (option === '--with-input-yaml' && !yamlPath && cds.utils.exists(cds.utils.path.join(cds.root, yamlPath)))
2940
return _usage(`Input yaml path ${yamlPath} does not exist.`)
3041

3142
await generateRuntimeValues(option, yamlPath)
@@ -35,7 +46,7 @@ async function capOperatorPlugin(cmd, option, yamlPath) {
3546
if (option === '--with-runtime-yaml' && !yamlPath)
3647
return _usage(`Input runtime yaml path is missing.`)
3748

38-
if (option === '--with-runtime-yaml' && !yamlPath && cds.utils.exists(cds.utils.path.join(cds.root,yamlPath)))
49+
if (option === '--with-runtime-yaml' && !yamlPath && cds.utils.exists(cds.utils.path.join(cds.root, yamlPath)))
3950
return _usage(`Input runtime yaml path ${yamlPath} does not exist.`)
4051

4152
await convertToconfigurableTemplateChart(option, yamlPath)
@@ -81,13 +92,13 @@ EXAMPLES
8192
}
8293

8394
async function transformRuntimeValues(runtimeYamlPath) {
84-
console.log('Transforming runtime values file '+ cds.utils.path.join(cds.root,runtimeYamlPath) + ' to the configurable template chart format.')
95+
console.log('Transforming runtime values file ' + cds.utils.path.join(cds.root, runtimeYamlPath) + ' to the configurable template chart format.')
8596
let runtimeYaml = yaml.parse(await cds.utils.read(cds.utils.path.join(cds.root, runtimeYamlPath)))
8697
if (runtimeYaml?.workloads?.server?.deploymentDefinition?.env) {
8798
const index = runtimeYaml.workloads.server.deploymentDefinition.env.findIndex(e => e.name === 'CDS_CONFIG')
8899
if (index > -1) {
89100
const cdsConfigValueJson = JSON.parse(runtimeYaml.workloads.server.deploymentDefinition.env[index].value)
90-
if (cdsConfigValueJson?.requires?.['cds.xt.DeploymentService']?.hdi?.create?.database_id){
101+
if (cdsConfigValueJson?.requires?.['cds.xt.DeploymentService']?.hdi?.create?.database_id) {
91102
runtimeYaml['hanaInstanceId'] = cdsConfigValueJson.requires['cds.xt.DeploymentService'].hdi.create.database_id
92103
delete runtimeYaml['workloads']
93104
await cds.utils.write(yaml.stringify(runtimeYaml)).to(cds.utils.path.join(cds.root, runtimeYamlPath))
@@ -102,36 +113,55 @@ async function isRuntimeValueAlreadyTransformed(runtimeYamlPath) {
102113
}
103114

104115
async function convertToconfigurableTemplateChart(option, runtimeYamlPath) {
105-
if (!((cds.utils.exists('chart') && isCAPOperatorChart(cds.utils.path.join(cds.root,'chart')))))
116+
if (!((cds.utils.exists('chart') && isCAPOperatorChart(cds.utils.path.join(cds.root, 'chart')))))
106117
throw new Error("No CAP Operator chart found in the project. Please run 'cds add cap-operator --force' to add the CAP Operator chart folder.")
107118

108-
if (isConfigurableTemplateChart(cds.utils.path.join(cds.root,'chart'))){
119+
if (isConfigurableTemplateChart(cds.utils.path.join(cds.root, 'chart'))) {
109120
console.log("Exisiting chart is already a configurable template chart. No need for conversion.")
110121
if (option === '--with-runtime-yaml' && runtimeYamlPath && !(await isRuntimeValueAlreadyTransformed(runtimeYamlPath)))
111122
await transformRuntimeValues(runtimeYamlPath)
112123
else
113-
console.log('Runtime values file '+ cds.utils.path.join(cds.root,runtimeYamlPath) + ' already in the configurable template chart format.')
124+
console.log('Runtime values file ' + cds.utils.path.join(cds.root, runtimeYamlPath) + ' already in the configurable template chart format.')
114125
return
115126
}
116127

117-
console.log('Converting chart '+cds.utils.path.join(cds.root,'chart')+' to configurable template chart.')
128+
console.log('Converting chart ' + cds.utils.path.join(cds.root, 'chart') + ' to configurable template chart.')
118129

119130
// Copy templates
120-
await cds.utils.copy(cds.utils.path.join(__dirname, '../files/configurableTemplatesChart/templates/_helpers.tpl')).to(cds.utils.path.join(cds.root,'chart/templates/_helpers.tpl'))
121-
await cds.utils.copy(cds.utils.path.join(__dirname, '../files/commonTemplates/')).to(cds.utils.path.join(cds.root,'chart/templates/'))
131+
await cds.utils.copy(cds.utils.path.join(__dirname, '../files/commonTemplates/')).to(cds.utils.path.join(cds.root, 'chart/templates/'))
122132

123-
isServiceOnlyChart(cds.utils.path.join(cds.root,'chart')) ? await cds.utils.copy(cds.utils.path.join(__dirname, '../files/configurableTemplatesChart/templates/cap-operator-cros-svc.yaml')).to(cds.utils.path.join(cds.root,'chart/templates/cap-operator-cros.yaml')) :
124-
await cds.utils.copy(cds.utils.path.join(__dirname, '../files/configurableTemplatesChart/templates/cap-operator-cros.yaml')).to(cds.utils.path.join(cds.root,'chart/templates/cap-operator-cros.yaml'))
133+
const valuesYaml = yaml.parse(await cds.utils.read(cds.utils.path.join(cds.root, 'chart/values.yaml')))
134+
const hasIas = getServiceInstanceKeyName(valuesYaml['serviceInstances'], 'identity') != null
135+
const hasXsuaa = getServiceInstanceKeyName(valuesYaml['serviceInstances'], 'xsuaa') != null
136+
137+
// Create _helpers.tpl
138+
await cds.utils.write(getHelperTpl({
139+
hasXsuaa: hasXsuaa
140+
}, false)).to(cds.utils.path.join(cds.root, 'chart/templates/_helpers.tpl'))
141+
142+
// Create domain.yaml
143+
await cds.utils.write(getDomainCroYaml({
144+
hasIas: hasIas
145+
})).to(cds.utils.path.join(cds.root, 'chart/templates/domain.yaml'))
146+
147+
// Create cap-operator-cros.yaml
148+
// Only filling those fields in the project input struct that are required to create CAPApplication CR
149+
// Workloads will be filled during transformValuesAndFillCapOpCroYaml function call
150+
await cds.utils.write(getConfigurableCapOpCroYaml({
151+
hasXsuaa: hasXsuaa,
152+
hasIas: hasIas,
153+
isService: isServiceOnlyChart(cds.utils.path.join(cds.root, 'chart'))
154+
})).to(cds.utils.path.join(cds.root, 'chart/templates/cap-operator-cros.yaml'))
125155

126156
// Copy values.schema.json
127-
await cds.utils.copy(cds.utils.path.join(__dirname, '../files/configurableTemplatesChart/values.schema.json')).to(cds.utils.path.join(cds.root,'chart', 'values.schema.json'))
157+
await cds.utils.copy(cds.utils.path.join(__dirname, '../files/configurableTemplatesChart/values.schema.json')).to(cds.utils.path.join(cds.root, 'chart', 'values.schema.json'))
128158

129159
// Add annotation to chart.yaml
130160
const chartYaml = yaml.parse(await cds.utils.read(cds.utils.path.join(cds.root, 'chart/Chart.yaml')))
131161
chartYaml['annotations']['app.kubernetes.io/part-of'] = 'cap-operator-configurable-templates'
132162
await cds.utils.write(yaml.stringify(chartYaml)).to(cds.utils.path.join(cds.root, 'chart/Chart.yaml'))
133163

134-
// Transform
164+
// Transform CAPApplicationVersion CR from values.yaml
135165
await transformValuesAndFillCapOpCroYaml()
136166

137167
if (option === '--with-runtime-yaml' && runtimeYamlPath) {
@@ -140,14 +170,14 @@ async function convertToconfigurableTemplateChart(option, runtimeYamlPath) {
140170
}
141171

142172
async function generateRuntimeValues(option, inputYamlPath) {
143-
if (!((cds.utils.exists('chart') && isCAPOperatorChart(cds.utils.path.join(cds.root,'chart'))))) {
173+
if (!((cds.utils.exists('chart') && isCAPOperatorChart(cds.utils.path.join(cds.root, 'chart'))))) {
144174
throw new Error("No CAP Operator chart found in the project. Please run 'cds add cap-operator --force' to add the CAP Operator chart folder.")
145175
}
146176

147177
let answerStruct = {}
148178
const { appName, appDescription } = getAppDetails()
149-
const isConfigurableTempChart = isConfigurableTemplateChart(cds.utils.path.join(cds.root,'chart'))
150-
const isServiceOnly = isServiceOnlyChart(cds.utils.path.join(cds.root,'chart'))
179+
const isConfigurableTempChart = isConfigurableTemplateChart(cds.utils.path.join(cds.root, 'chart'))
180+
const isServiceOnly = isServiceOnlyChart(cds.utils.path.join(cds.root, 'chart'))
151181

152182
if (option === '--with-input-yaml' && inputYamlPath) {
153183

@@ -190,8 +220,16 @@ async function generateRuntimeValues(option, inputYamlPath) {
190220
const valuesYaml = yaml.parse(await cds.utils.read(cds.utils.path.join(cds.root, 'chart/values.yaml')))
191221

192222
//get saas-registry and xsuaa service keys
193-
answerStruct['saasRegistryKeyName'] = getServiceInstanceKeyName(valuesYaml['serviceInstances'], 'saas-registry') || 'saas-registry'
194-
answerStruct['xsuaaKeyName'] = getServiceInstanceKeyName(valuesYaml['serviceInstances'], 'xsuaa') || 'xsuaa'
223+
const xsuaaServiceInstanceKey = getServiceInstanceKeyName(valuesYaml['serviceInstances'], 'xsuaa')
224+
if (xsuaaServiceInstanceKey == null) {
225+
answerStruct['hasXsuaa'] = false
226+
answerStruct['subscriptionManagerKeyName'] = getServiceInstanceKeyName(valuesYaml['serviceInstances'], 'subscription-manager') || 'subscription-manager'
227+
answerStruct['identityKeyName'] = getServiceInstanceKeyName(valuesYaml['serviceInstances'], 'identity') || 'identity'
228+
} else {
229+
answerStruct['hasXsuaa'] = true
230+
answerStruct['saasRegistryKeyName'] = getServiceInstanceKeyName(valuesYaml['serviceInstances'], 'saas-registry') || 'saas-registry'
231+
answerStruct['xsuaaKeyName'] = getServiceInstanceKeyName(valuesYaml['serviceInstances'], 'xsuaa') || 'xsuaa'
232+
}
195233

196234
answerStruct['isApp'] = !isServiceOnly
197235
answerStruct['isService'] = isServiceOnly
@@ -220,7 +258,7 @@ function updateWorkloadEnv(runtimeValuesYaml, valuesYaml, answerStruct) {
220258

221259
const cdsConfigHana = Mustache.render('{"requires":{"cds.xt.DeploymentService":{"hdi":{"create":{"database_id":"{{hanaInstanceId}}"}}}}}', answerStruct)
222260

223-
if ((workloadDetails?.deploymentDefinition?.type === 'CAP' || workloadDetails?.deploymentDefinition?.type === 'service') && answerStruct['hanaInstanceId']) {
261+
if ((workloadDetails?.deploymentDefinition?.type === 'CAP' || workloadDetails?.deploymentDefinition?.type === 'Service') && answerStruct['hanaInstanceId']) {
224262
updateCdsConfigEnv(runtimeValuesYaml, workloadKey, 'deploymentDefinition', cdsConfigHana)
225263
}
226264

@@ -242,14 +280,6 @@ function updateWorkloadEnv(runtimeValuesYaml, valuesYaml, answerStruct) {
242280
}
243281
}
244282

245-
function getServiceInstanceKeyName(serviceInstances, offeringName) {
246-
for (const key in serviceInstances) {
247-
if (serviceInstances[key].serviceOfferingName === offeringName)
248-
return key
249-
}
250-
return null
251-
}
252-
253283
function updateCdsConfigEnv(runtimeValuesYaml, workloadKey, workloadDefintion, cdsConfigHana) {
254284
const index = runtimeValuesYaml['workloads'][workloadKey][workloadDefintion]['env'].findIndex(e => e.name === 'CDS_CONFIG')
255285
if (index > -1) {
@@ -285,7 +315,7 @@ async function getShootDomain() {
285315

286316
kubectl.on('close', () => { resolve() })
287317
})
288-
} catch (error) {}
318+
} catch (error) { }
289319

290320
return domain
291321
}

files/approuter.yaml.hbs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,22 @@ workloads:
66
consumedBTPServices:
77
{{#hasXsuaa}}
88
- {{appName}}-uaa-bind
9+
{{#isApp}}
10+
- {{appName}}-saas-registry-bind
11+
{{/isApp}}
912
{{/hasXsuaa}}
13+
{{#hasIas}}
14+
- {{appName}}-identity-bind
15+
{{#isApp}}
16+
- {{appName}}-subscription-manager-bind
17+
{{/isApp}}
18+
{{/hasIas}}
1019
{{#hasDestination}}
1120
- {{appName}}-destination-bind
1221
{{/hasDestination}}
1322
{{#hasHTML5Repo}}
1423
- {{appName}}-html5-repo-runtime-bind
1524
{{/hasHTML5Repo}}
16-
{{#hasMultitenancy}}
17-
- {{appName}}-saas-registry-bind
18-
{{/hasMultitenancy}}
1925
deploymentDefinition:
2026
type: Router
2127
image:

files/commonTemplates/service-instance.yaml

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,20 @@ spec:
3838

3939
{{- if or $serviceSpec.parameters $serviceSpec.jsonParameters }}
4040
{{- $parameters := $serviceSpec.parameters | default (dict) }}
41-
{{- if and (eq $serviceSpec.serviceOfferingName "xsuaa") (or (not (get (get $parameters "oauth2-configuration" | default (dict)) "redirect-uris")) (eq (len (get (get $parameters "oauth2-configuration" | default (dict)) "redirect-uris")) 0)) }}
42-
{{- $redirectUris := include "redirectUris" $ | fromJson }}
43-
{{- $oauth2Cfg := merge ($redirectUris) (get $parameters "oauth2-configuration" | default (dict)) }}
44-
{{- $_ := set $parameters "oauth2-configuration" $oauth2Cfg }}
45-
{{- end }}
4641
{{- $jsonParameters := $serviceSpec.jsonParameters | default "{}" }}
42+
43+
{{- if or (eq $serviceSpec.serviceOfferingName "xsuaa") (eq $serviceSpec.serviceOfferingName "identity") }}
44+
{{- $missingRedirects := or
45+
(not (get (get $parameters "oauth2-configuration" | default (dict)) "redirect-uris"))
46+
(eq (len (get (get $parameters "oauth2-configuration" | default (dict)) "redirect-uris")) 0)
47+
-}}
48+
49+
{{- if $missingRedirects }}
50+
{{- $redirectUris := include "redirectUris" (dict "serviceOfferingName" $serviceSpec.serviceOfferingName "context" $) | fromJson }}
51+
{{- $oauth2Cfg := merge $redirectUris (get $parameters "oauth2-configuration" | default (dict)) }}
52+
{{- $_ := set $parameters "oauth2-configuration" $oauth2Cfg }}
53+
{{- end }}
54+
{{- end }}
4755
parameters: {{- $jsonParameters | fromJson | merge $parameters | toYaml | nindent 4 }}
4856
{{- end }}
4957

files/configurableTemplatesChart/values.yaml.hbs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,7 @@ workloads:
4141
{{/isApp}}
4242
contentDeploy:
4343
image:
44+
{{#hasAms}}
45+
amsDeployer:
46+
image:
47+
{{/hasAms}}

files/ias.yaml.hbs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
serviceInstances:
2+
identity:
3+
name: {{appName}}-identity
4+
serviceOfferingName: identity
5+
servicePlanName: application
6+
parameters:
7+
display-name: {{appName}}
8+
multi-tenant: true
9+
xsuaa-cross-consumption: true
10+
{{#hasAms}}
11+
authorization:
12+
enabled: true
13+
{{/hasAms}}
14+
oauth2-configuration:
15+
redirect-uris: []
16+
post-logout-redirect-uris: []
17+
home-url: ""
18+
19+
serviceBindings:
20+
identity:
21+
name: {{appName}}-identity-bind
22+
serviceInstanceName: {{appName}}-identity
23+
secretName: {{appName}}-identity-bind-secret
24+
secretKey: credentials
25+
parameters:
26+
credential-type: X509_GENERATED
27+
app-identifier: {{appName}}

files/runtime-values.yaml.hbs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
serviceInstances:
2+
{{#hasXsuaa}}
3+
{{#isApp}}
24
{{saasRegistryKeyName}}:
35
parameters:
46
xsappname: {{appName}}
@@ -13,10 +15,37 @@ serviceInstances:
1315
getDependencies: "https://{{appName}}.{{clusterDomain}}/callback/v1.0/dependencies"
1416
{{/isService}}
1517
onSubscription: "https://{{capOperatorSubdomain}}.{{clusterDomain}}/provision/tenants/{tenantId}"
18+
{{/isApp}}
1619
{{xsuaaKeyName}}:
1720
parameters:
1821
xsappname: {{appName}}
19-
22+
{{/hasXsuaa}}
23+
{{^hasXsuaa}}
24+
{{#isApp}}
25+
{{subscriptionManagerKeyName}}:
26+
parameters:
27+
appName: {{appName}}
28+
commercialAppName: {{appName}}
29+
displayName: ({{appName}})
30+
description: ({{appDescription}})
31+
category: CAP
32+
appCallbacks:
33+
dependenciesCallbacks:
34+
url: "https://{{providerSubdomain}}.{{appName}}.{{clusterDomain}}/v1.0/callback/tenants/{app_tid}/dependencies"
35+
subscriptionCallbacks:
36+
url: "https://{{capOperatorSubdomain}}.{{clusterDomain}}/sms/provision/tenants/{app_tid}"
37+
{{/isApp}}
38+
{{identityKeyName}}:
39+
parameters:
40+
display-name: {{appName}}
41+
home-url: "https://$BTP_SUBDOMAIN.{{appName}}.{{clusterDomain}}"
42+
{{/hasXsuaa}}
43+
{{^hasXsuaa}}
44+
serviceBindings:
45+
{{identityKeyName}}:
46+
parameters:
47+
app-identifier: {{appName}}
48+
{{/hasXsuaa}}
2049
app:
2150
domains:
2251
primary: {{appName}}.{{clusterDomain}}

files/saas-registry.yaml.hbs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ serviceInstances:
1010
displayName: ({{appName}})
1111
description: ({{appDescription}})
1212
appUrls:
13+
getDependencies: ""
14+
onSubscription: ""
1315
callbackTimeoutMillis: 300000
1416
onSubscriptionAsync: true
1517
onUnSubscriptionAsync: true
16-
category: "CAP"
18+
category: CAP
1719

1820
serviceBindings:
1921
saasRegistry:
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
serviceInstances:
2+
subscriptionManager:
3+
name: {{appName}}-subscription-manager
4+
serviceOfferingName: subscription-manager
5+
servicePlanName: provider
6+
parameters:
7+
iasServiceInstanceName: {{appName}}-identity
8+
applicationType: application
9+
appName: {{appName}}
10+
commercialAppName: {{appName}}
11+
displayName: ({{appName}})
12+
description: ({{appDescription}})
13+
category: CAP
14+
appCallbacks:
15+
dependenciesCallbacks:
16+
url: ""
17+
subscriptionCallbacks:
18+
url: ""
19+
async:
20+
subscribeEnable: true
21+
unSubscribeEnable: true
22+
timeoutInMillis: 300000
23+
24+
serviceBindings:
25+
subscriptionManager:
26+
name: {{appName}}-subscription-manager-bind
27+
serviceInstanceName: {{appName}}-subscription-manager
28+
secretName: {{appName}}-subscription-manager-bind-secret
29+
secretKey: credentials
30+
parameters: {}

0 commit comments

Comments
 (0)