@sap/cds-dk 8.7.2 → 8.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +39 -3
  2. package/bin/bind.js +7 -8
  3. package/bin/cds.js +2 -4
  4. package/bin/compile/index.js +2 -1
  5. package/bin/deploy/index.js +10 -5
  6. package/bin/lint.js +10 -4
  7. package/bin/watch.js +48 -7
  8. package/lib/bind/{cfKeyProvider.js → cf.js} +43 -53
  9. package/lib/bind/k8s.js +138 -0
  10. package/lib/bind/plugin.js +48 -0
  11. package/lib/bind/shared.js +267 -0
  12. package/lib/build/buildTaskEngine.js +1 -1
  13. package/lib/build/plugins/helm/files/subcharts/content-deployment/Chart.yaml +0 -2
  14. package/lib/build/plugins/helm/files/subcharts/web-application/Chart.yaml +1 -3
  15. package/lib/build/plugins/helm/files/subcharts/web-application/templates/service-binding.yaml +3 -0
  16. package/lib/build/plugins/helm/files/subcharts/web-application/values.schema.json +3 -0
  17. package/lib/build/plugins/helm/files/subcharts/web-application/values.yaml +2 -1
  18. package/lib/build/util.js +1 -1
  19. package/lib/compile/to-mermaid.js +1 -2
  20. package/lib/deploy/to-hana/hana.js +4 -4
  21. package/lib/deploy/to-hana/mtaUtil.js +0 -13
  22. package/lib/import/common.js +6 -7
  23. package/lib/init/constants.js +1 -1
  24. package/lib/init/merge.js +42 -33
  25. package/lib/init/projectReader.js +15 -7
  26. package/lib/init/registries/mta.js +9 -3
  27. package/lib/init/template/approuter/files/mta.yaml.hbs +2 -2
  28. package/lib/init/template/approuter/files/package.json +1 -1
  29. package/lib/init/template/approuter/files/xs-app.json.hbs +0 -11
  30. package/lib/init/template/approuter/index.js +2 -2
  31. package/lib/init/template/cloud-logging/files/package.json.hbs +12 -1
  32. package/lib/init/template/cloud-logging/files/values.yaml.hbs +0 -2
  33. package/lib/init/template/cloud-logging/index.js +8 -15
  34. package/lib/init/template/destination/files/values.yaml +4 -4
  35. package/lib/init/template/destination/index.js +1 -1
  36. package/lib/init/template/html5-repo/files/app-package.json.hbs +0 -2
  37. package/lib/init/template/html5-repo/files/containerize.yaml.hbs +25 -0
  38. package/lib/init/template/html5-repo/files/mta.yaml.hbs +1 -2
  39. package/lib/init/template/html5-repo/files/package.json.hbs +1 -1
  40. package/lib/init/template/html5-repo/files/ui-resources/package.json.hbs +11 -0
  41. package/lib/init/template/html5-repo/files/ui5-deploy.yaml.hbs +4 -7
  42. package/lib/init/template/html5-repo/files/values.yaml.hbs +6 -4
  43. package/lib/init/template/html5-repo/index.js +22 -10
  44. package/lib/init/template/http/index.js +1 -1
  45. package/lib/init/template/ias/files/mta.yaml.hbs +1 -1
  46. package/lib/init/template/ias/files/sidecar.package.json.hbs +12 -0
  47. package/lib/init/template/ias/index.js +1 -3
  48. package/lib/init/template/multitenancy/files/mta.yaml.hbs +8 -10
  49. package/lib/init/template/multitenancy/index.js +1 -1
  50. package/lib/init/template/portal/files/mta.yaml.hbs +9 -0
  51. package/lib/init/template/portal/index.js +3 -1
  52. package/lib/init/template/sample/files/app/admin-books/webapp/manifest.json +119 -119
  53. package/lib/init/template/sample/files/app/browse/webapp/manifest.json +1 -1
  54. package/lib/init/template/telemetry/index.js +0 -13
  55. package/lib/init/template/typer/files/package.json +3 -0
  56. package/lib/init/template/workzone-standard/files/cdm.json.hbs +97 -0
  57. package/lib/init/template/workzone-standard/files/mta.yaml.hbs +23 -0
  58. package/lib/init/template/workzone-standard/index.js +70 -39
  59. package/lib/init/template/xsuaa/files/mta.yaml.hbs +2 -2
  60. package/lib/init/template/xsuaa/files/values.yaml.hbs +0 -2
  61. package/lib/init/template/xsuaa/index.js +22 -10
  62. package/lib/lint/Linter.js +242 -201
  63. package/lib/lint/io.js +4 -7
  64. package/lib/mtx/util/cf.js +1 -2
  65. package/lib/util/cf.js +104 -84
  66. package/lib/util/command.js +1 -2
  67. package/lib/util/git.js +1 -1
  68. package/npm-shrinkwrap.json +60 -43
  69. package/package.json +1 -1
  70. package/bin/deploy/to-cf.js +0 -0
  71. package/bin/deploy/via-mta.js +0 -0
  72. package/lib/bind/KubernetesApiAccessor.js +0 -81
  73. package/lib/bind/bindingManager.js +0 -89
  74. package/lib/bind/bindingResolver.js +0 -54
  75. package/lib/bind/bindingUtil.js +0 -129
  76. package/lib/bind/index.js +0 -127
  77. package/lib/bind/kubernetesCredentialsProvider.js +0 -84
  78. package/lib/bind/userCdsrcJsonStorageProvider.js +0 -92
  79. package/lib/init/template/destination/files/package.sidecar.json +0 -7
  80. package/lib/init/template/html5-repo/files/package.sidecar.json +0 -7
  81. package/lib/init/template/telemetry/files/package.json.hbs +0 -18
package/CHANGELOG.md CHANGED
@@ -7,7 +7,43 @@ This project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  The format is based on [Keep a Changelog](https://keepachangelog.com/).
9
9
 
10
- ## Version 8.7.2 - 2024-02-14
10
+ ## Version 8.8.0 - 2025-03-03
11
+
12
+ ### Added
13
+
14
+ - `cds add xsuaa` lets you pass a `--plan` option, e.g. for `cds add xsuaa --plan broker`.
15
+ - `cds add workzone` and `cds add workzone-standard` support for Kyma.
16
+ - `cds add typer` now adds a `before:cds-watch` script to run cds-typer before starting `cds watch`.
17
+ - `cds watch` supports a `before:cds-watch` npm script in your `package.json`, executed once before the initial `cds watch` startup.
18
+
19
+ ### Changed
20
+
21
+ - Running `cds deploy` in dry mode with an output file specified will now only produce a warning in stderr and will not exit with an error code.
22
+ - `cds deploy --out …` will not generate the specified output file if it would end up empty.
23
+ - `cds add workzone` adds a transpilation task for UI5 deployment descriptors in TypeScript projects.
24
+ - `cds add workzone` does not use the deprecated `webide-extension-task-updateManifestJson` task any more.
25
+
26
+ ### Fixed
27
+
28
+ - `cds add approuter` doesn't create entries for `app` and `appconfig` local directories any more.
29
+ - `cds add telemetry` for Java doesn't erroneously add `cds` configuration or `dependencies` to the `package.json`.
30
+ - `cds add sample` adds the workzone-specific configuration if `cds.requires.workzone` is `true`.
31
+ - `cds compile -o` uses the correct file suffix if explicitly specified in the file name.
32
+ - `cds bind` caches the promise of its `cf -v` call to prevent race conditions.
33
+ - `cds import --out <folder>` does not fail if executed in a folder with a `.`.
34
+ - `cds add workzone` has improved support for multitenancy.
35
+
36
+ ## Version 8.7.3 - 2025-02-19
37
+
38
+ ### Fixed
39
+
40
+ - `cds compile -o` fixes the output file name in `-o <service>.json`.
41
+ - `cds lint` won't stumble over scalar config objects anymore.
42
+ - `cds init` uses latest Maven Java archetype version 3.7.2 for creating Java projects.
43
+ - `cds deploy --to hana --dry` doesn't exit with a `TypeError` if there are no models.
44
+ - `cds add ias` fixes a few scenarios in combination with multitenancy.
45
+
46
+ ## Version 8.7.2 - 2025-02-14
11
47
 
12
48
  ### Fixed
13
49
 
@@ -21,7 +57,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
21
57
  - `cds bind -a` doesn't concurrently try to check the Cloud Foundry version or OAuth token.
22
58
  - `cds import` now resolves target of association/composition correctly for multiple schema files.
23
59
 
24
- ## Version 8.7.1 - 2024-02-04
60
+ ## Version 8.7.1 - 2025-02-04
25
61
 
26
62
  ### Fixed
27
63
 
@@ -1574,7 +1610,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
1574
1610
  - `cds init` uses latest Maven Java archetype version 1.22.2 for creating Java projects.
1575
1611
  - `cds import` modified documentation for namespace option.
1576
1612
  - `cds import` does not create bound function imports key parameters in CSN for OData V2.
1577
- - `cds import` now when `--keep-namespace` option is not given validates the file name and then converts it to complier supported format as service name .
1613
+ - `cds import` now when `--keep-namespace` option is not given validates the file name and then converts it to compiler supported format as service name .
1578
1614
  - add new methods from `FsUtil` to typescript interface.
1579
1615
 
1580
1616
  ### Fixed
package/bin/bind.js CHANGED
@@ -69,6 +69,7 @@ module.exports = Object.assign(cds_bind, {
69
69
  cds bind --to bookshop-db --for my-profile
70
70
 
71
71
  cds bind --to-app-services bookshop-srv
72
+ cds bind -a bookshop-srv
72
73
 
73
74
  cds bind messaging --to redis-cache --credentials '{"host":"localhost","port":6379}'
74
75
  cds bind messaging --to redis-cache --credentials ./credentials.json
@@ -78,21 +79,21 @@ module.exports = Object.assign(cds_bind, {
78
79
  const os = require('os');
79
80
  const IS_WIN = os.platform() === 'win32';
80
81
 
81
- const { bind, storeBindings, parseProfile } = require('../lib/bind');
82
+ const { bind, storeBindings } = require('../lib/bind/shared');
82
83
 
83
84
 
84
85
  async function cds_bind(args, options) {
85
86
  const { on, 'to-app-services': app, to, out, 'output-file': outputFile, exec, credentials } = options
86
87
 
87
88
  if (exec) {
88
- process.env.CDS_ENV = parseProfile(process.env.CDS_ENV);
89
+ process.env.CDS_ENV ??= 'hybrid'
89
90
  if (options.for) {
90
91
  console.warn(term.warn(`Parameter --for is not supported in combination with --exec. Its value will be ignored.`));
91
92
  }
92
93
  return cds_bind_exec(args)
93
94
  }
94
95
 
95
- // Check if cds command was bind. Don't abort in case of deploy
96
+ // Check if command was cds bind. Don't abort for cds deploy.
96
97
  if (process.env.CDS_ENV && process.argv[2] === 'bind') {
97
98
  throw `Option ${term.bold('--profile')} is no longer supported. Use ${term.bold('--for')} instead to specify the profile to use when storing connection settings.`;
98
99
  }
@@ -156,7 +157,7 @@ async function cds_bind(args, options) {
156
157
  if (app) {
157
158
  const cds = require('../lib/cds');
158
159
  const resolvedServices = [], unresolvedServices = {}
159
- const services = (await require('../lib/util/cf').getServices(app)).filter(s => Object.keys(s.credentials).length > 0)
160
+ const services = (await require('../lib/util/cf').services(app)).filter(s => Object.keys(s.credentials).length > 0)
160
161
 
161
162
  services.forEach(service => {
162
163
  const type = service.label || service.type;
@@ -213,10 +214,8 @@ async function cds_bind_exec(command) { // eslint-disable-lint
213
214
  const cds = require('../lib/cds');
214
215
  const env = cds.env.for('cds', process.cwd());
215
216
 
216
- const BindingManager = require('../lib/bind/bindingManager')
217
- const bindingManager = new BindingManager({ env })
218
- const bindingEnv = await bindingManager.bindingEnv()
219
- const processEnv = Object.assign(process.env, bindingEnv)
217
+ const { bindingEnv } = require('../lib/bind/shared')
218
+ const processEnv = Object.assign(process.env, await bindingEnv({env}))
220
219
 
221
220
  execAndExit({ env: processEnv }, ...command);
222
221
  }
package/bin/cds.js CHANGED
@@ -171,10 +171,8 @@ const DEBUG = /\b(y|all|cli)\b/.test(process.env.DEBUG) ? console.debug : undefi
171
171
 
172
172
  async function _resolveBindings(options) {
173
173
  const cds = require('../lib/cds')
174
- const BindingManager = require('../lib/bind/bindingManager')
175
- const bindingManager = new BindingManager(options)
176
- const bindingEnv = await bindingManager.bindingEnv()
177
- Object.assign(process.env, bindingEnv)
174
+ const { bindingEnv } = require('../lib/bind/shared')
175
+ Object.assign(process.env, await bindingEnv(options))
178
176
  cds.env = cds.env.for('cds') // reload env // FIXME: This is too early!
179
177
  cds.requires = cds.env.requires // REVISIT: We should clean this up!
180
178
  }
@@ -224,6 +224,7 @@ async function compile (models, options={}) {
224
224
  if (ext) {
225
225
  options.file ??= path.basename(dest, ext)
226
226
  options.dest = path.dirname(dest)
227
+ options.suffix ??= ext
227
228
  }
228
229
  }
229
230
  if (options.all) return compile_all (models[0])
@@ -294,7 +295,7 @@ async function compile (models, options={}) {
294
295
  const write = require ('../../lib/util/write')
295
296
  chain = chain.then (write.to ({
296
297
  folder: options.dest,
297
- file: options.file || options.service === 'all' ? path.basename(src) : options.service,
298
+ file: options.file || (options.service === 'all' ? path.basename(src) : options.service),
298
299
  suffix: options.suffix || suffix4(_suffix),
299
300
  [options.dest ? 'foreach' : 'log']: options.log || consoleLog
300
301
  }))
@@ -159,14 +159,19 @@ async function deploy(_model, options) { // NOSONAR
159
159
  const log = this.log || _log
160
160
  if (options.messages.length) log (options.messages, options)
161
161
 
162
- if (options.out) {
162
+ if (options.out && options.schema_log?.length) {
163
163
  await cds.utils.write (options.out, options.schema_log.join('\n'))
164
164
  }
165
165
  } catch (e) {
166
- if (e.code === 'MODEL_NOT_FOUND') {
167
- if (!_model) throw new Error('No .cds models found, please specify one as an argument')
168
- throw new Error(`No cds models found at/in '${_model}'`)
169
- } else throw e
166
+ if (e.code !== 'MODEL_NOT_FOUND') throw e
167
+ if (_model) throw `No cds models found at/in '${_model}'`
168
+ if (options.dry && options.out) {
169
+ // this is to allow project starts w/o a model with a watcher running
170
+ console.error('No .cds models found. Doing nothing in dry mode with specified output file.')
171
+ }
172
+ else {
173
+ throw 'No .cds models found, please specify one as an argument'
174
+ }
170
175
  }
171
176
  }
172
177
 
package/bin/lint.js CHANGED
@@ -1,7 +1,13 @@
1
1
  const Linter = require('../lib/lint/Linter');
2
2
  const linter = new Linter();
3
3
 
4
- module.exports = Object.assign(
5
- linter.lint,
6
- linter.init()
7
- );
4
+ // cds cli exec expects an executable object with various additional properties.
5
+ // As most of the linting functionality is bound to a class instance, we need to wrap the instance
6
+ // to not lose the this-context and attach the additional properties to the function.
7
+ const lint = (...args) => linter.lint(...args)
8
+ const additionalProperties = Object.entries(linter.init())
9
+ .map(([key, value]) => [key, { value }])
10
+
11
+ Object.defineProperties(lint, Object.fromEntries(additionalProperties))
12
+
13
+ module.exports = lint
package/bin/watch.js CHANGED
@@ -1,3 +1,6 @@
1
+ const path = require('node:path')
2
+ const { execSync } = require('node:child_process')
3
+
1
4
  const watchOnlyOptions = ['--ext', '--livereload', '--open', '--debug', '--inspect', '--inspect-brk', '--include', '--exclude']
2
5
 
3
6
  module.exports = Object.assign ( watch, {
@@ -68,7 +71,7 @@ module.exports = Object.assign ( watch, {
68
71
  `})
69
72
 
70
73
  const cds = require('../lib/cds')
71
- const DEBUG = cds.log('cli|watch')._debug
74
+ const DEBUG = cds.log('cli|watch')
72
75
  const {delimiter,dirname,relative,resolve,sep} = require ('path')
73
76
  const {codes} = require('../lib/util/term'), t = codes
74
77
  const fs = require ('fs')
@@ -161,17 +164,17 @@ async function watch ([cwd], {
161
164
  ? new RegExp(ignoreDefaults.map(d => d.source).concat(excludedPaths).join('|'))
162
165
  : new RegExp(ignoreDefaults.map(d => d.source).join('|'))
163
166
  const watched = [ _cwd, ...included.map(resolved) ]
164
- if (DEBUG) {
167
+ if (DEBUG._debug) {
165
168
  console.log(`\nwatching (pid ${process.pid})`, {
166
169
  extensions:ext, roots:watched, ignoring: [...excludedPaths.map(s => new RegExp(s)), ...ignoreDefaults]
167
170
  })
168
171
  }
169
172
  if (liveReload) console.log (`live reload enabled for browsers`)
170
173
 
171
- const BindingManager = require('../lib/bind/bindingManager')
172
- const bindingManager = new BindingManager({cwd})
173
- const bindingEnv = await bindingManager.bindingEnv()
174
- if (bindingEnv) env = {...env, ...bindingEnv}
174
+ const { bindingEnv, updateBindings } = require('../lib/bind/shared')
175
+ Object.assign(env, await bindingEnv({cwd}))
176
+
177
+ runBeforeScript('watch', {cwd})
175
178
 
176
179
  const nodemon = require('../lib/watch/node-watch')
177
180
  const watcher = nodemon ({
@@ -183,7 +186,7 @@ async function watch ([cwd], {
183
186
  if (liveReload) liveReload.markPending(files)
184
187
  if (this.delayed) clearTimeout (this.delayed)
185
188
  this.delayed = setTimeout(()=>{
186
- bindingManager.update({
189
+ updateBindings({
187
190
  onBeforeUpdate: () => watcher.stop(),
188
191
  onAfterUpdate: ({env}) => {
189
192
  Object.keys(env).forEach( name => watcher.setEnv(name, env[name]) )
@@ -248,3 +251,41 @@ function installed(id, ...paths) {
248
251
  try { return require.resolve (id, { paths }) }
249
252
  catch(e) { if (e.code !== 'MODULE_NOT_FOUND') throw e }
250
253
  }
254
+
255
+ /**
256
+ * Runs a before-script for the given task.
257
+ * For now, this will only be used for cds watch,
258
+ * but could be used for other tasks in the future,
259
+ * therefore the generic name/ implementation.
260
+ * @param {string} task - The task to run the before-script for.
261
+ * @param {object} options - Options object containing the cwd.
262
+ * @param {[string]} options.cwd - The current working directory.
263
+ */
264
+ function runBeforeScript(task, {cwd}) {
265
+ cwd ??= '.'
266
+ try {
267
+ const packageJson = getPackageJson(cwd)
268
+ const beforeScript = `before:cds-${task}`
269
+ if (packageJson.scripts?.[beforeScript]) {
270
+ DEBUG(`running before-script '${beforeScript}' for cds ${task}`)
271
+ const cmd = `npm run ${beforeScript} --prefix "${cwd}"`
272
+ execSync(cmd)
273
+ }
274
+ } catch (e) {
275
+ // ignore not finding package.json (happens during cds init)
276
+ if (!(e.code === 'MODULE_NOT_FOUND' && e.message.includes('package.json'))) throw e
277
+ }
278
+ }
279
+
280
+ /**
281
+ * @param {string} project - The project to get the package.json from.
282
+ * @returns {JSON} The package.json of the given project.
283
+ */
284
+ function getPackageJson (project) {
285
+ try {
286
+ return require(path.join(project, 'package.json'))
287
+ } catch (e) {
288
+ if (e.code !== 'MODULE_NOT_FOUND') throw e
289
+ return require(path.join(cds.root, project, 'package.json'))
290
+ }
291
+ }
@@ -1,52 +1,47 @@
1
1
  const cds = require('../cds');
2
2
  const DEBUG = cds.log('cli')._debug
3
-
4
- const cfUtil = require('../util/cf');
3
+ const cf = require('../util/cf');
5
4
  const { bold } = require('../util/term');
6
- const { mergeCredentials } = require('./index');
5
+ const { mergeCredentials } = require('./shared');
7
6
 
8
- module.exports = new class CFKeyProvider {
9
- async init() {
10
- if (!this.target) {
11
- this.target = await cfUtil.getCfSpaceInfo();
12
- }
13
- }
7
+ module.exports = new class CFBind extends require('./plugin') {
14
8
 
15
- async getOrg(orgName) {
16
- const result = await cfUtil.cfRequest('/v3/organizations', { names: orgName });
17
- return result?.resources?.[0];
18
- }
9
+ /* Public API */
19
10
 
20
- async getSpace(orgGuid, spaceName) {
21
- const result = await cfUtil.cfRequest('/v3/spaces', { names: spaceName, organization_guids: orgGuid });
22
- return result?.resources?.find(r => r.relationships.organization.data.guid === orgGuid);
11
+ displayName() {
12
+ return 'Cloud Foundry'
23
13
  }
24
14
 
25
- async getInstance(spaceGuid, instanceName) {
26
- const result = await cfUtil.cfRequest('/v3/service_instances', { names: instanceName, space_guids: spaceGuid });
27
- return result?.resources?.find(r => !!r.relationships.space.data.guid);
15
+ binding4(service) {
16
+ const [instance, key] = service.split(/:/)
17
+ return { instance, key }
28
18
  }
29
19
 
30
- async getPlan(planGuid) {
31
- const result = await cfUtil.cfRequest('/v3/service_plans', { guids: planGuid });
32
- return result?.resources?.[0];
20
+ description4(binding) {
21
+ if (binding.key) {
22
+ return `managed service ${bold(binding.instance + ':' + binding.key)}`
23
+ } else {
24
+ return `user provided service ${bold(binding.instance)}`
25
+ }
33
26
  }
34
27
 
35
- async getOffering(offeringGuid) {
36
- const result = await cfUtil.cfRequest('/v3/service_offerings', { guids: offeringGuid });
37
- return result?.resources?.[0];
28
+ service4({ instance, key }, services) {
29
+ const toServices = Array.isArray(services) ? services.map(s => s.binding.instance) : services.split(',')
30
+ const service = toServices.find(s => s === instance || s === instance + ':' + key)
31
+ if (!service) throw `service '${instance}:${key}' not found`
32
+ return service
38
33
  }
39
34
 
40
35
  async resolve(name, binding) {
41
- await this.init();
42
- this._checkApiEndpoint(name, binding)
36
+ this.target ??= await cf.target()
37
+ this.#checkApiEndpoint(name, binding)
43
38
 
44
39
  let { instance, org, space } = binding;
45
40
  let spaceGuid, orgGuid;
46
41
  if (org && space) {
47
42
  // org and space can be overwritten by currently logged in org and space
48
- const orgObj = await this.getOrg(org);
49
- const spaceObj = await this.getSpace(orgObj.guid, space);
43
+ const orgObj = await cf.org(org);
44
+ const spaceObj = await cf.space(orgObj.guid, space);
50
45
  spaceGuid = spaceObj.guid;
51
46
  orgGuid = orgObj.guid;
52
47
  } else {
@@ -58,41 +53,43 @@ module.exports = new class CFKeyProvider {
58
53
 
59
54
  let instanceObj;
60
55
  try {
61
- instanceObj = await this.getInstance(spaceGuid, instance);
56
+ instanceObj = await cf.instance(spaceGuid, instance);
62
57
  const instanceSpaceGuid = instanceObj.relationships.space.data.guid;
63
58
  if (instanceSpaceGuid !== spaceGuid) {
64
59
  // service instance is shared from another space, service key must be created in that space
65
- const sharedFromSpaceObj = await cfUtil.cfRequest('/v3/spaces/' + encodeURIComponent(instanceSpaceGuid));
60
+ const sharedFromSpaceObj = await cf.curl('/v3/spaces/' + encodeURIComponent(instanceSpaceGuid));
66
61
  space = sharedFromSpaceObj.name;
67
62
  spaceGuid = sharedFromSpaceObj.guid;
68
63
  const instanceOrgGuid = sharedFromSpaceObj.relationships.organization.data.guid;
69
64
  if (instanceOrgGuid !== orgGuid) {
70
- const sharedFromOrgObj = await cfUtil.cfRequest('/v3/organizations/' + encodeURIComponent(instanceOrgGuid));
65
+ const sharedFromOrgObj = await cf.curl('/v3/organizations/' + encodeURIComponent(instanceOrgGuid));
71
66
  org = sharedFromOrgObj.name;
72
67
  orgGuid = sharedFromOrgObj.guid;
73
68
  }
74
69
  }
75
70
  } catch (error) {
76
- throw `Service instance ${bold(instance)} not found.`;
71
+ throw `service instance ${bold(instance)} not found.`;
77
72
  }
78
73
  switch (instanceObj.type) {
79
74
  case 'managed':
80
- return this._resolveManagedService(instanceObj, instance, binding.key || `${instance}-key`, org, space, binding.credentials);
75
+ return this.#resolveManagedService(instanceObj, instance, binding.key || `${instance}-key`, org, space, binding.credentials);
81
76
  case 'user-provided':
82
- return this._resolveUserProvidedService(instanceObj, instance, org, space, binding.credentials);
77
+ return this.#resolveUserProvidedService(instanceObj, instance, org, space, binding.credentials);
83
78
  default:
84
79
  throw `Service type ${bold(instanceObj.type)} not supported`;
85
80
  }
86
81
  }
87
82
 
88
- async _resolveManagedService(instanceObj, instance, key, org, space, customCredentials) {
83
+ /* Private helpers */
84
+
85
+ async #resolveManagedService(instanceObj, instance, key, org, space, customCredentials) {
89
86
  const create = (cds.cli.command === 'bind' || cds.cli.command === 'deploy') && !cds.cli.options['no-create-service-key']
90
87
  const silent = (cds.cli.options.json || cds.cli.command === 'env') && !DEBUG
91
- const planObj = await this.getPlan(instanceObj.relationships.service_plan.data.guid);
92
- const offeringObj = await this.getOffering(planObj.relationships.service_offering.data.guid);
88
+ const planObj = await cf.plan(instanceObj.relationships.service_plan.data.guid);
89
+ const offeringObj = await cf.offering(planObj.relationships.service_offering.data.guid);
93
90
  const parameters = offeringObj.name === "identity" ? { "credential-type": "X509_GENERATED", "app-identifier" : "cds_bind" } : {};
94
91
 
95
- const credentials = await cfUtil.getOrCreateServiceKey(instanceObj, key, parameters, { create, silent });
92
+ const credentials = await cf.getOrCreateServiceKey(instanceObj, key, parameters, { create, silent });
96
93
  if (!credentials) {
97
94
  let message = `No service key ${bold(key)} found for service instance ${bold(instance)}.\n\n`;
98
95
  message += `Use ${bold(`cf create-service-key ${instanceObj.name} ${key} [-c ...]`)} to create the required service key.`;
@@ -103,16 +100,12 @@ module.exports = new class CFKeyProvider {
103
100
  binding: {
104
101
  type: "cf",
105
102
  apiEndpoint: this.target.apiEndpoint,
106
- org: org,
107
- space: space,
108
- instance: instance,
109
- key: key,
103
+ org, space, instance, key,
110
104
  vcap: {
111
105
  label: offeringObj.name,
112
106
  plan: planObj.name,
113
107
  tags: offeringObj.tags
114
- },
115
- resolved: true,
108
+ }
116
109
  },
117
110
  credentials: mergeCredentials(credentials, customCredentials)
118
111
  };
@@ -120,27 +113,24 @@ module.exports = new class CFKeyProvider {
120
113
  return resolvedBinding;
121
114
  }
122
115
 
123
- async _resolveUserProvidedService(instanceObj, instance, org, space, customCredentials) {
124
- const credentials = await cfUtil.cfRequest(`/v3/service_instances/${encodeURIComponent(instanceObj.guid)}/credentials`);
116
+ async #resolveUserProvidedService(instanceObj, instance, org, space, customCredentials) {
117
+ const credentials = await cf.curl(`/v3/service_instances/${encodeURIComponent(instanceObj.guid)}/credentials`);
125
118
  const resolvedBinding = {
126
119
  binding: {
127
120
  type: "cf",
128
121
  apiEndpoint: this.target.apiEndpoint,
129
- org: org,
130
- space: space,
131
- instance: instance,
122
+ org, space, instance,
132
123
  vcap: {
133
124
  label: instanceObj.type,
134
125
  tags: instanceObj.tags
135
- },
136
- resolved: true,
126
+ }
137
127
  },
138
128
  credentials: mergeCredentials(credentials, customCredentials)
139
129
  };
140
130
  return resolvedBinding;
141
131
  }
142
132
 
143
- _checkApiEndpoint(name, binding) {
133
+ #checkApiEndpoint(name, binding) {
144
134
  const { apiEndpoint, key, instance } = binding
145
135
 
146
136
  // if no api endpoint is configured the currently logged in endpoint will be used
@@ -0,0 +1,138 @@
1
+ const { execSync } = require('node:child_process')
2
+ const { yaml } = require('../cds').utils
3
+ const { mergeCredentials } = require('./shared')
4
+ const { bold } = require('../util/term')
5
+
6
+ module.exports = new class K8SBind extends require('./plugin') {
7
+
8
+ /* Public API */
9
+
10
+ displayName() {
11
+ return 'Kubernetes'
12
+ }
13
+
14
+ description4(binding) {
15
+ if (binding.name) {
16
+ return `service binding ${bold(binding.name)}`
17
+ } else {
18
+ return `secret ${bold(binding.secret)}`
19
+ }
20
+ }
21
+
22
+ binding4(service) {
23
+ return { name: service }
24
+ }
25
+
26
+ service4({ secret, name }, services) {
27
+ const toServices = Array.isArray(services) ? services.map(s => s.binding.name || s.binding.secret) : services.split(',')
28
+ const service = toServices.find(s => s === secret || s === name)
29
+ if (!service) throw `secret or binding '${secret || name}' not found`
30
+ return service
31
+ }
32
+
33
+ async resolve(name, binding) {
34
+ const { context, namespace, cluster } = await this.#context4(binding)
35
+
36
+ let { name: bindingName, secret, credentials: customCredentials } = binding
37
+
38
+ let instance
39
+ let secretOrBindingResource = 'secret'
40
+
41
+ if (bindingName) {
42
+ const bindingResource = await this.#data4({ context, namespace, kind: 'servicebindings', name: bindingName })
43
+ if (bindingResource === null) {
44
+ secret = bindingName // Fallback to secret instead of service binding
45
+ bindingName = undefined
46
+ secretOrBindingResource = 'service binding or secret'
47
+ } else {
48
+ const { secretName, serviceInstanceName } = bindingResource.spec
49
+ if (!secretName || !serviceInstanceName) throw 'secret name or service instance name not found in service binding.'
50
+ secret = secretName
51
+ instance = serviceInstanceName
52
+ }
53
+ }
54
+
55
+ const secretResource = await this.#data4({ context, namespace, kind: 'secrets', name: secret })
56
+ if (!secretResource) throw `${secretOrBindingResource} '${secret}' not found.`
57
+
58
+ const properties = Object.fromEntries(
59
+ Object.entries(secretResource.data).map(([key, value]) => [key, Buffer.from(value, 'base64').toString()])
60
+ )
61
+
62
+ // REVISIT: Proper API
63
+ // eslint-disable-next-line cds-dk/use-relative-require, cds-dk/use-api-require
64
+ const serviceBinding = require('@sap/cds/lib/env/serviceBindings').parseBinding('/', name, properties)
65
+ const { credentials } = serviceBinding
66
+ const vcap = { ...serviceBinding, credentials: undefined }
67
+
68
+ const resolvedBinding = {
69
+ binding: {
70
+ type: 'k8s',
71
+ name: bindingName,
72
+ cluster, instance, namespace, secret,
73
+ vcap
74
+ },
75
+ credentials: mergeCredentials(credentials, customCredentials)
76
+ }
77
+ return resolvedBinding
78
+ }
79
+
80
+ /* Private helpers */
81
+
82
+ async #data4({ context, namespace, kind, name }) {
83
+ try {
84
+ return JSON.parse(execSync(`kubectl --output json --context ${context} --namespace ${namespace} get ${kind} ${name}`).toString())
85
+ }
86
+ catch (error) {
87
+ if (error.status === 1) return null
88
+ else throw error
89
+ }
90
+ }
91
+
92
+ #configPromise
93
+ async #context4({ cluster, namespace }) {
94
+ if (!this.#configPromise) {
95
+ this.#configPromise = (async () => {
96
+ const raw = execSync('kubectl config view --raw').toString()
97
+ const { clusters, contexts, 'current-context': current } = yaml.parse(raw)
98
+ if (!current) throw 'no current context in kubectl config.'
99
+ const entry = contexts?.find(entry => entry.name === current && entry.context?.cluster && entry.context?.user)
100
+ if (!entry) throw 'current context not found in kubectl config.'
101
+ const clusterEntry = clusters?.find(clusterItem => clusterItem.name === entry.context?.cluster && clusterItem.cluster?.server)
102
+ if (!clusterEntry) throw `cluster ${bold(entry.cluster)} not found in kubectl config.`
103
+ const { cluster } = clusterEntry
104
+
105
+ const serverToContext = {}
106
+ for (const { name, cluster } of clusters) {
107
+ const contextEntries = contexts.filter(entry => entry.context.cluster === name)
108
+ // Collect only cluster servers that are used in exactly one context, to clearly identify the context to be used
109
+ if (contextEntries.length === 1) {
110
+ serverToContext[cluster.server] = contextEntries[0]
111
+ }
112
+ }
113
+
114
+ return {
115
+ namespace: current.namespace || 'default',
116
+ context: entry,
117
+ cluster,
118
+ serverToContext
119
+ }
120
+ })()
121
+ }
122
+ const config = await this.#configPromise
123
+ let contextEntry
124
+
125
+ if (cluster && cluster !== config.cluster.server) {
126
+ const context = config.serverToContext[cluster]
127
+ if (context) contextEntry = context
128
+ else throw `cluster ${bold(cluster)} not found in kubectl configuration`
129
+ } else {
130
+ contextEntry = config.context
131
+ }
132
+ return {
133
+ context: contextEntry.name,
134
+ cluster: cluster || config.cluster.server,
135
+ namespace: namespace || contextEntry.context.namespace || 'default'
136
+ }
137
+ }
138
+ }
@@ -0,0 +1,48 @@
1
+ module.exports = class BindPlugin {
2
+
3
+ /**
4
+ * Returns the display name of the binding plugin
5
+ * @returns {string} The display name
6
+ */
7
+ displayName() {
8
+ throw new Error('displayName not implemented')
9
+ }
10
+
11
+ /**
12
+ * Returns a description for the given binding
13
+ * @param {Object} binding - The binding configuration object
14
+ * @returns {string} A description of the binding
15
+ */
16
+ description4(_binding) {
17
+ throw new Error('description4 not implemented')
18
+ }
19
+
20
+ /**
21
+ * Parses the [service]:[key] string into a binding configuration object
22
+ * @param {string} service - The passed service instance name
23
+ * @returns {Object} The parsed binding configuration
24
+ */
25
+ binding4(_service) {
26
+ throw new Error('binding4 not implemented')
27
+ }
28
+
29
+ /**
30
+ * Returns a description for the given binding
31
+ * @param {Object} binding - The binding configuration object
32
+ * @param {Object} services – Service candidates
33
+ * @returns {string} The name of the service matching the binding
34
+ */
35
+ service4(_binding, _services) {
36
+ throw new Error('credentials4 not implemented')
37
+ }
38
+
39
+ /**
40
+ * Resolves the binding configuration for the given service name
41
+ * @param {string} name - The name of the service
42
+ * @param {Object} binding - The binding configuration object
43
+ * @returns {Promise<{binding: Object, credentials: Object}>} A promise that resolves to the resolved binding
44
+ */
45
+ async resolve(_name, _binding) {
46
+ throw new Error('resolve not implemented')
47
+ }
48
+ }