@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
268 lines (246 loc) • 10.2 kB
JavaScript
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
}