UNPKG

@sap/cds-dk

Version:

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

268 lines (246 loc) 10.2 kB
const os = require('node:os') const cds = require('../cds') const { exists, read, fs, path } = cds.utils const git = require('../util/git') const { bold } = require('../util/term') const KindToRequiresNameMap = { xsuaa: 'auth', 'xsuaa-auth': 'auth', 'ias-auth': 'auth', 'hana-cloud': 'db', hana: 'db', 'hana-mt': 'db', // For compatibility only 'sql-mt': 'db', // For compatibility only destinations: 'destinations', connectivity: 'connectivity', 'audit-log': 'auditlog', 'audit-log-to-restv2': 'auditlog', 'enterprise-messaging': 'messaging', 'enterprise-messaging-amqp': 'messaging', 'enterprise-messaging-http': 'messaging', 'enterprise-messaging-shared': 'messaging', 'redis-messaging': 'messaging', 'multitenancy': 'multitenancy' } const PreferredKinds = { multitenancy: 1, 'hana-cloud': 1, xsuaa: 1, 'xsuaa-auth': 1, 'ias-auth': 1, 'saas-registry': 1 } async function bind(options = { on: 'cf' }) { let on switch (options.on) { case 'k8s': case 'kubernetes': on = 'k8s' break case undefined: case null: case 'cf': case 'cloudfoundry': on = 'cf' break default: throw `invalid value '${options.on}' for option --on.` } const plugin = require(`./${on}`) if (!options['to-app-services']) console.log(`retrieving data from ${bold(plugin.displayName())}...`) const services = options.to.split(/,/g) const resolvedServices = await Promise.all(services.map(async service => { const binding = { on, ...plugin.binding4(service) } const resolved = await _resolveBinding(binding) delete resolved.credentials return resolved })) if (options.kind) { resolvedServices.forEach(service => service.kind = options.kind) } else { _determineKinds(resolvedServices) } for (const resolved of resolvedServices) { const { kind, binding, binding: { instance, secret } } = resolved if (options.serviceArg) { resolved.name = options.serviceArg } else if (kind) { const requiresName = KindToRequiresNameMap[kind] if (!requiresName) throw new Error(`Unknown CDS service name for service ${bold(instance)}. Please specify as argument for ${bold('cds bind')}.`) resolved.name = requiresName } resolved.name ??= `custom-service:${instance || secret}` const kindText = kind ? ` with kind ${bold(kind)}` : '' console.log(`binding ${bold(resolved.name)} to ${plugin.displayName()} ${_bindingText(binding)}${kindText}`) } return resolvedServices } async function storeBindings(resolvedServices, options) { const cdsrcPrivate = '.cdsrc-private.json' let jsonPath = options.out || cdsrcPrivate if (jsonPath === cdsrcPrivate) { await git.ensureFileIsGitignored(cdsrcPrivate) } jsonPath = jsonPath.replace(/~/g, os.homedir()) if (path.extname(jsonPath) !== '.json') { jsonPath = path.join(jsonPath, '.cdsrc.json') } const conf = exists(jsonPath) ? await read(jsonPath) : {} let cdsRoot if (path.basename(jsonPath) === 'package.json') { conf.cds = conf.cds || {} cdsRoot = conf.cds } else { cdsRoot = conf } const profile = `[${options.for}]` const features = cdsRoot.features?.[profile] if (features) delete features.emulate_vcap_services cdsRoot.requires ??= {} const section = cdsRoot.requires[profile] ??= {} for (const { name, kind, binding, credentials } of resolvedServices) { const requireService = section[name] ??= {} binding.credentials = _getServiceCredentials(binding, resolvedServices, options) Object.assign(requireService, { binding: { ...binding, vcap: undefined }, credentials, kind, vcap: { name } }) } await fs.promises.mkdir(path.dirname(jsonPath), { recursive: true }) await fs.promises.writeFile(jsonPath, JSON.stringify(conf, null, 2)) await fs.promises.chmod(jsonPath, 0o600) console.log(`saving bindings to ${bold(jsonPath)} in profile ${bold(options.for)}`) } function mergeCredentials(credentials, customCredentials) { if (!customCredentials) return credentials for (const key in customCredentials) { if (typeof customCredentials[key] === 'object') { if (credentials[key] && typeof credentials[key] !== 'object') { throw `Cannot merge credentials: '${key}' is not an object` } if (!credentials[key]) { credentials[key] = {} } credentials[key] = mergeCredentials(credentials[key], customCredentials[key]) } else { credentials[key] = customCredentials[key] } } return credentials } async function bindingEnv(options) { const bindings = _getUnresolvedBindings(options) const resolvedBindings = await _resolveBindings({ ...options, bindings, cache: {} }) return resolvedBindings ? { VCAP_SERVICES: _toVcapServices(resolvedBindings) } : undefined } async function updateBindings(options, { onBeforeUpdate, onAfterUpdate } = {}) { const bindings = _getUnresolvedBindings(options) if (onBeforeUpdate) await onBeforeUpdate() let resolvedBindings try { resolvedBindings = await _resolveBindings({ ...options, bindings, cache: {} }) } catch (error) { console.error(error.message) // REVISIT: Why not throw here? return } const env = resolvedBindings ? { VCAP_SERVICES: _toVcapServices(resolvedBindings) } : { VCAP_SERVICES: '{}' } if (onAfterUpdate) await onAfterUpdate({ bindings: resolvedBindings, env }) return { bindings: resolvedBindings, env } } module.exports = { bind, storeBindings, mergeCredentials, bindingEnv, updateBindings } /* Private helpers */ function _bindingText(binding) { try { return require(`./${binding.on ?? binding.type}`).description4(binding) } catch (e) { if (e.code === 'MODULE_NOT_FOUND') throw `unsupported binding type ${bold(binding.on ?? binding.type)}.` } } function _resolveBinding(binding, name) { try { return require(`./${binding.on ?? binding.type}`).resolve(name, binding) } catch (e) { if (e.code === 'MODULE_NOT_FOUND') throw `unsupported binding type ${bold(binding.on ?? binding.type)} for service ${bold(name)}.` } } function _determineKinds(services) { const vcapServices = {} // Build VCAP_SERVICES for (const service of services) { service.kindCandidates = [] const { vcap = {} } = service.binding || {} const { label, type = label } = vcap if (!type) continue if (!vcapServices[type]) vcapServices[type] = [] vcapServices[type].push({ credentials: { dummy: 'dummy' }, ...vcap, service }) } // Auto configure services based on VCAP_SERVICES const cds = require('../cds') const env = cds.env.for('cds', process.cwd()) if (!env._find_credentials_for_required_service) { // REVISIT: Proper API throw new Error(`Please provide a service kind or update @sap/cds version to use the kind detection.`) } const requires = env.requires.kinds || {} for (const [kind, service] of Object.entries(requires)) { const vcapService = env._find_credentials_for_required_service(kind, service, vcapServices) if (!vcapService) continue const isUniqueKind = !Object.values(requires).some(otherService => otherService !== service && otherService.kind === kind ) if (isUniqueKind) vcapService.service.kindCandidates.push(kind) } for (const service of services) { const preferredKind = service.kindCandidates.find(kind => kind in PreferredKinds) if (preferredKind) { service.kind = preferredKind } else if (service.kindCandidates.length === 1) { service.kind = service.kindCandidates[0] } delete service.kindCandidates } } function _getUnresolvedBindings({ cwd = process.cwd(), env } = {}) { if (!env) { env = cds.env.for('cds', path.resolve(cwd)) // REVISIT: This might load cds.env too early! } const bindings = {} const requires = env.requires || {} for (const name in requires) { const service = requires[name] if (service?.binding && !service.binding.resolved) { bindings[name] = { ...service.binding, kind: service.kind } } } return bindings } async function _resolveBindings({ bindings, cwd = process.cwd(), env, cache = {}, silent }) { bindings ??= _getUnresolvedBindings({ cwd, env }) if (Object.keys(bindings).length === 0) return undefined if (!silent) console.log(`resolving cloud service bindings...`) const resolvedBindings = {} await Promise.all(Object.keys(bindings).map(async name => { const binding = bindings[name] const cacheKey = JSON.stringify(binding) let resolvedBinding if (!cache[cacheKey]) { resolvedBinding = cache[cacheKey] = await _resolveBinding(binding, name) if (!silent) console.log(`bound ${bold(name)} to ${binding.type} ${_bindingText(binding)}`) } else { resolvedBinding = cache[cacheKey] } resolvedBindings[name] = resolvedBinding })) return Object.keys(resolvedBindings).length > 0 ? resolvedBindings : undefined } function _toVcapServices(bindings) { const services = {} for (const name in bindings) { const { binding, credentials } = bindings[name] const vcap = binding.vcap ?? {} const type = vcap.type ?? vcap.label services[type] ??= [] services[type].push({ ...vcap, name, credentials }) } return JSON.stringify(services) } function _getServiceCredentials(binding, services, options) { const { to, credentials } = options const { type } = binding if (!credentials) return const plugin = require(`./${type}`) const service = plugin.service4(binding, to ?? services) return credentials[service] ?? credentials }