@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
237 lines (183 loc) • 9.79 kB
JavaScript
const term = require('../lib/util/term')
module.exports = Object.assign(cds_bind, {
options: ['--to', '--for', '--on', '--kind', '--out', '--to-app-services', '--credentials', '--output-file'],
flags: ['--exec', '--no-create-service-key'],
shortcuts: ['-2', '-4', '-n', '-k', '-o', '-a', '-c'],
help: `
# SYNOPSIS
*cds bind* <service> [<options>]
*cds bind --exec* [--profile <profile>] [--] <command> <args ...>
Binds the given service to a service instance by storing connection
information in *.cdsrc-private.json*
in your project directory. Credentials are not stored in the file but rather
retrieved dynamically during *cds watch*.
Use option *--out* to specify a different path or file.
With *--exec* you can execute arbitrary commands with your service bindings.
The service bindings are provided
in the *VCAP_SERVICES* env variable to the command.
# OPTIONS
*-2 | --to* <instance>[:<key>] | <service-binding> | <secret>
Bind to a given Cloud Foundry instance, Kubernetes service binding or Kubernetes secret.
*-4 | --for* <profile>
Profile to store binding information. Defaults to *hybrid*.
*-n | --on* cf | k8s
Target platform (Cloud Foundry or Kubernetes) to bind to.
Defaults to *k8s* when Helm charts are present and no *mta.yaml* exists, otherwise *cf*.
*-k | --kind* <kind>
Kind of the service.
*-o | --out* <path>
Output file for added binding information. Use this option if binding configuration should
be added to *package.json* or *.cdsrc.json*. If *path* is a directory, the *.cdsrc.json*
file in that directory is used. Defaults to *.cdsrc-private.json*.
*-a | --to-app-services* <app>
Bind services of a given application.
*--no-create-service-key*
Skip automatic creation of service keys.
*-c | --credentials* <JSON|{file path}>
JSON or file path defining custom fields overwriting service credentials.
Custom credentials are merged with the credentials read from BTP when running your application in hybrid mode.
# EXAMPLE
cds bind --to bookshop-db
cds bind --to bookshop-db,bookshop-auth
cds bind auth --to bookshop-auth:bookshop-auth-key --kind xsuaa --for my-profile
cds bind --to bookshop-db --out .cdsrc.json
cds bind --to bookshop-db --on k8s
cds bind --to bookshop-db --for my-profile
cds bind --to-app-services bookshop-srv
cds bind -a bookshop-srv
cds bind messaging --to redis-cache --credentials '{"host":"localhost","port":6379}'
cds bind messaging --to redis-cache --credentials ./credentials.json
`
});
const os = require('os');
const IS_WIN = os.platform() === 'win32';
const cds = require('../lib/cds')
const { bind, storeBindings } = require('../lib/bind/shared');
const { readProject } = require('../lib/init/projectReader')
async function cds_bind(args, options) {
const { hasKyma, hasMta } = readProject()
const onKyma = cds.cli.options.on === 'k8s' || (hasKyma && !hasMta && cds.cli.options.on !== 'cf')
const { on = onKyma ? 'k8s' : 'cf', 'to-app-services': app, to, out, 'output-file': outputFile, exec, credentials } = options
if (exec) {
process.env.CDS_ENV ??= 'hybrid'
if (options.for) {
console.warn(term.warn(`Parameter --for is not supported in combination with --exec. Its value will be ignored.`));
}
return cds_bind_exec(args)
}
// Check if command was cds bind. Don't abort for cds deploy.
if (process.argv.includes('--profile') && process.argv[2] === 'bind') {
throw `Option ${term.bold('--profile')} is no longer supported. Use ${term.bold('--for')} instead to specify the profile to use when storing connection settings.`;
}
if (outputFile) {
console.warn(term.warn(`Option --output-file is deprecated. Please use --out instead.`))
options.out ??= outputFile
}
if (credentials === true) {
throw `Option ${term.bold('--credentials')} requires a JSON object or a file path.`;
}
if (!to && !app) {
// we currently don't support binding to a service without specifying the target instance
// const cds = require('../lib/cds');
// if (args[0] && cds.env.requires[args[0]]?.vcap?.name) {
// options.to = cds.env.requires[args[0]].vcap.name
// } else {
throw `Use option ${term.bold('--to')} or ${term.bold('-2')} to specify the target instance, e.g. ${term.bold('cds bind --to myInstance:myService')}`;
// }
}
// read credentials from file if it is a file, from JSON if it is JSON
if (credentials) {
let credentialsJson = options.credentials;
if (!credentials.trim().startsWith('{')) {
const fs = require('fs');
try {
credentialsJson = fs.readFileSync(credentials, 'utf8');
} catch (e) {
throw `Error reading credentials from file ${credentials}: ${e.message}`
}
}
try {
options.credentials = JSON.parse(credentialsJson);
} catch (e) {
throw `Invalid JSON format for option ${term.bold('--credentials')}: ${e.message}`;
}
if (options.to) {
//at least one service must match
const toServices = options.to.split(',');
if (toServices.length > 1) {
if (!toServices.some(toService => options.credentials[toService])) {
throw `Option ${term.bold('--credentials')} cannot uniquely identify a matching service defined in ${term.bold('--to')}.\nWhen passing multiple services, include the service name in the credentials object.\nExample: cds bind --to bookshop-db,bookshop-redis --credentials '{ "bookshop-redis": {"host": "localhost", "port": 1234}}'`
}
}
}
}
if (app === true) {
throw `Option ${term.bold('--to-app-services')} requires an app name, for example ${term.bold('cds bind --to-app-services bookshop-srv')}.`
}
options.for ??= 'hybrid';
if (app) {
const cds = require('../lib/cds')
const resolvedServices = [], unresolvedServices = {}
const plugin = require(`../lib/bind/${on}`)
const services = await plugin.services4(app)
if (!services) console.warn(`no services bound to app ${app} - nothing to bind`)
services.forEach(service => {
const type = service.label || service.type;
const requiredServiceEntry = Object.entries(cds.env.requires).find(([, requiredService]) => requiredService.kind === type && requiredService?.vcap?.name === service.name)
if (requiredServiceEntry) {
resolvedServices.push({ options: { ...options, ...{ to: service.name, serviceArg: requiredServiceEntry[0] } } })
} else {
// check whether we can resolve this service
// multiple services of the same type cannot be resolved
if (services.some(s => (s.label || s.type) === type && s.name != service.name)) {
unresolvedServices[type] ??= [];
unresolvedServices[type].push(service);
} else {
resolvedServices.push({ options: { ...options, ...{ to: service.name } } })
}
}
})
const resolvedServiceBindings = (await Promise.all(resolvedServices.map(service => bind(service.options, service.options.to, out)))).flat();
if (resolvedServiceBindings.length > 0) {
await storeBindings(resolvedServiceBindings, options);
}
Object.values(unresolvedServices)
.filter(services => services.length > 1)
.forEach(services => {
console.warn(`Found multiple service instances of the same type ${services[0].label || services[0].type}. This is not supported.`);
console.warn('Instead, bind each service individually, e.g.:');
services.forEach((service, idx) => console.warn(` cds bind <your local service name ${idx + 1}> --to ${service.name}`));
});
} else {
if (args.length > 1) {
throw 'Too many arguments: Please specify only one or no service.';
}
options.serviceArg = args[0]
options.targets = options.to.split(/,/g);
if (options.targets.length >= 2 && options.serviceArg) {
throw `Service argument cannot be specified together with multiple target (${term.bold('--to')}) services. Use one service per call or omit the service argument.`;
}
if (options.targets.length >= 2 && options.kind) {
throw `The option '--kind' cannot be specified together with multiple target (${term.bold('--to')}) services. Use one service per call or omit the ${term.bold('--kind')} option.`;
}
const services = await bind({ ...options, out });
await storeBindings(services, options);
}
}
async function cds_bind_exec(command) { // eslint-disable-lint
const cds = require('../lib/cds');
const env = cds.env.for('cds', process.cwd());
const { env: bindingEnv } = require('../lib/bind/shared')
const processEnv = Object.assign(process.env, await bindingEnv({env}))
execAndExit({ env: processEnv }, ...command);
}
function execAndExit(options, command, ...args) {
const { spawnSync } = require('child_process');
// use shell with Windows only; without output is not visible and some commands cannot be run
const result = spawnSync(command, args, {
stdio: 'inherit',
shell: IS_WIN,
...options
});
process.exit(result.status);
}