UNPKG

@sap/cds-dk

Version:

Command line client and development toolkit for the SAP Cloud Application Programming Model

237 lines (183 loc) 9.79 kB
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); }