@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.
- package/CHANGELOG.md +39 -3
- package/bin/bind.js +7 -8
- package/bin/cds.js +2 -4
- package/bin/compile/index.js +2 -1
- package/bin/deploy/index.js +10 -5
- package/bin/lint.js +10 -4
- package/bin/watch.js +48 -7
- package/lib/bind/{cfKeyProvider.js → cf.js} +43 -53
- package/lib/bind/k8s.js +138 -0
- package/lib/bind/plugin.js +48 -0
- package/lib/bind/shared.js +267 -0
- package/lib/build/buildTaskEngine.js +1 -1
- package/lib/build/plugins/helm/files/subcharts/content-deployment/Chart.yaml +0 -2
- package/lib/build/plugins/helm/files/subcharts/web-application/Chart.yaml +1 -3
- package/lib/build/plugins/helm/files/subcharts/web-application/templates/service-binding.yaml +3 -0
- package/lib/build/plugins/helm/files/subcharts/web-application/values.schema.json +3 -0
- package/lib/build/plugins/helm/files/subcharts/web-application/values.yaml +2 -1
- package/lib/build/util.js +1 -1
- package/lib/compile/to-mermaid.js +1 -2
- package/lib/deploy/to-hana/hana.js +4 -4
- package/lib/deploy/to-hana/mtaUtil.js +0 -13
- package/lib/import/common.js +6 -7
- package/lib/init/constants.js +1 -1
- package/lib/init/merge.js +42 -33
- package/lib/init/projectReader.js +15 -7
- package/lib/init/registries/mta.js +9 -3
- package/lib/init/template/approuter/files/mta.yaml.hbs +2 -2
- package/lib/init/template/approuter/files/package.json +1 -1
- package/lib/init/template/approuter/files/xs-app.json.hbs +0 -11
- package/lib/init/template/approuter/index.js +2 -2
- package/lib/init/template/cloud-logging/files/package.json.hbs +12 -1
- package/lib/init/template/cloud-logging/files/values.yaml.hbs +0 -2
- package/lib/init/template/cloud-logging/index.js +8 -15
- package/lib/init/template/destination/files/values.yaml +4 -4
- package/lib/init/template/destination/index.js +1 -1
- package/lib/init/template/html5-repo/files/app-package.json.hbs +0 -2
- package/lib/init/template/html5-repo/files/containerize.yaml.hbs +25 -0
- package/lib/init/template/html5-repo/files/mta.yaml.hbs +1 -2
- package/lib/init/template/html5-repo/files/package.json.hbs +1 -1
- package/lib/init/template/html5-repo/files/ui-resources/package.json.hbs +11 -0
- package/lib/init/template/html5-repo/files/ui5-deploy.yaml.hbs +4 -7
- package/lib/init/template/html5-repo/files/values.yaml.hbs +6 -4
- package/lib/init/template/html5-repo/index.js +22 -10
- package/lib/init/template/http/index.js +1 -1
- package/lib/init/template/ias/files/mta.yaml.hbs +1 -1
- package/lib/init/template/ias/files/sidecar.package.json.hbs +12 -0
- package/lib/init/template/ias/index.js +1 -3
- package/lib/init/template/multitenancy/files/mta.yaml.hbs +8 -10
- package/lib/init/template/multitenancy/index.js +1 -1
- package/lib/init/template/portal/files/mta.yaml.hbs +9 -0
- package/lib/init/template/portal/index.js +3 -1
- package/lib/init/template/sample/files/app/admin-books/webapp/manifest.json +119 -119
- package/lib/init/template/sample/files/app/browse/webapp/manifest.json +1 -1
- package/lib/init/template/telemetry/index.js +0 -13
- package/lib/init/template/typer/files/package.json +3 -0
- package/lib/init/template/workzone-standard/files/cdm.json.hbs +97 -0
- package/lib/init/template/workzone-standard/files/mta.yaml.hbs +23 -0
- package/lib/init/template/workzone-standard/index.js +70 -39
- package/lib/init/template/xsuaa/files/mta.yaml.hbs +2 -2
- package/lib/init/template/xsuaa/files/values.yaml.hbs +0 -2
- package/lib/init/template/xsuaa/index.js +22 -10
- package/lib/lint/Linter.js +242 -201
- package/lib/lint/io.js +4 -7
- package/lib/mtx/util/cf.js +1 -2
- package/lib/util/cf.js +104 -84
- package/lib/util/command.js +1 -2
- package/lib/util/git.js +1 -1
- package/npm-shrinkwrap.json +60 -43
- package/package.json +1 -1
- package/bin/deploy/to-cf.js +0 -0
- package/bin/deploy/via-mta.js +0 -0
- package/lib/bind/KubernetesApiAccessor.js +0 -81
- package/lib/bind/bindingManager.js +0 -89
- package/lib/bind/bindingResolver.js +0 -54
- package/lib/bind/bindingUtil.js +0 -129
- package/lib/bind/index.js +0 -127
- package/lib/bind/kubernetesCredentialsProvider.js +0 -84
- package/lib/bind/userCdsrcJsonStorageProvider.js +0 -92
- package/lib/init/template/destination/files/package.sidecar.json +0 -7
- package/lib/init/template/html5-repo/files/package.sidecar.json +0 -7
- 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.
|
|
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 -
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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').
|
|
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
|
|
217
|
-
const
|
|
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
|
|
175
|
-
|
|
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
|
}
|
package/bin/compile/index.js
CHANGED
|
@@ -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
|
}))
|
package/bin/deploy/index.js
CHANGED
|
@@ -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
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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')
|
|
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
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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('./
|
|
5
|
+
const { mergeCredentials } = require('./shared');
|
|
7
6
|
|
|
8
|
-
module.exports = new class
|
|
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
|
-
|
|
16
|
-
const result = await cfUtil.cfRequest('/v3/organizations', { names: orgName });
|
|
17
|
-
return result?.resources?.[0];
|
|
18
|
-
}
|
|
9
|
+
/* Public API */
|
|
19
10
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return result?.resources?.find(r => r.relationships.organization.data.guid === orgGuid);
|
|
11
|
+
displayName() {
|
|
12
|
+
return 'Cloud Foundry'
|
|
23
13
|
}
|
|
24
14
|
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
return
|
|
15
|
+
binding4(service) {
|
|
16
|
+
const [instance, key] = service.split(/:/)
|
|
17
|
+
return { instance, key }
|
|
28
18
|
}
|
|
29
19
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
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
|
|
42
|
-
this
|
|
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
|
|
49
|
-
const spaceObj = await
|
|
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
|
|
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
|
|
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
|
|
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 `
|
|
71
|
+
throw `service instance ${bold(instance)} not found.`;
|
|
77
72
|
}
|
|
78
73
|
switch (instanceObj.type) {
|
|
79
74
|
case 'managed':
|
|
80
|
-
return this
|
|
75
|
+
return this.#resolveManagedService(instanceObj, instance, binding.key || `${instance}-key`, org, space, binding.credentials);
|
|
81
76
|
case 'user-provided':
|
|
82
|
-
return this
|
|
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
|
-
|
|
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
|
|
92
|
-
const offeringObj = await
|
|
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
|
|
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
|
|
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
|
|
124
|
-
const credentials = await
|
|
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
|
|
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
|
-
|
|
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
|
package/lib/bind/k8s.js
ADDED
|
@@ -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
|
+
}
|