UNPKG

@sap/cds-dk

Version:

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

139 lines (118 loc) 5 kB
const { execSync } = require('node:child_process') const { yaml } = require('../cds').utils const { mergeCredentials } = require('./shared') const { bold } = require('../util/term') module.exports = new class K8SBind extends require('./plugin') { /* Public API */ displayName() { return 'Kubernetes' } description4(binding) { if (binding.name) { return `service binding ${bold(binding.name)}` } else { return `secret ${bold(binding.secret)}` } } binding4(service) { return { name: service } } service4({ secret, name }, services) { const toServices = Array.isArray(services) ? services.map(s => s.binding.name || s.binding.secret) : services.split(',') const service = toServices.find(s => s === secret || s === name) if (!service) throw `secret or binding '${secret || name}' not found` return service } async resolve(name, binding) { const { context, namespace, cluster } = await this.#context4(binding) let { name: bindingName, secret, credentials: customCredentials } = binding let instance let secretOrBindingResource = 'secret' if (bindingName) { const bindingResource = await this.#data4({ context, namespace, kind: 'servicebindings', name: bindingName }) if (bindingResource === null) { secret = bindingName // Fallback to secret instead of service binding bindingName = undefined secretOrBindingResource = 'service binding or secret' } else { const { secretName, serviceInstanceName } = bindingResource.spec if (!secretName || !serviceInstanceName) throw 'secret name or service instance name not found in service binding.' secret = secretName instance = serviceInstanceName } } const secretResource = await this.#data4({ context, namespace, kind: 'secrets', name: secret }) if (!secretResource) throw `${secretOrBindingResource} '${secret}' not found.` const properties = Object.fromEntries( Object.entries(secretResource.data).map(([key, value]) => [key, Buffer.from(value, 'base64').toString()]) ) // REVISIT: Proper API // eslint-disable-next-line cds-dk/use-relative-require, cds-dk/use-api-require const serviceBinding = require('@sap/cds/lib/env/serviceBindings').parseBinding('/', name, properties) const { credentials } = serviceBinding const vcap = { ...serviceBinding, credentials: undefined } const resolvedBinding = { binding: { type: 'k8s', name: bindingName, cluster, instance, namespace, secret, vcap }, credentials: mergeCredentials(credentials, customCredentials) } return resolvedBinding } /* Private helpers */ async #data4({ context, namespace, kind, name }) { try { return JSON.parse(execSync(`kubectl --output json --context ${context} --namespace ${namespace} get ${kind} ${name}`).toString()) } catch (error) { if (error.status === 1) return null else throw error } } #configPromise async #context4({ cluster, namespace }) { if (!this.#configPromise) { this.#configPromise = (async () => { const raw = execSync('kubectl config view --raw').toString() const { clusters, contexts, 'current-context': current } = yaml.parse(raw) if (!current) throw 'no current context in kubectl config.' const entry = contexts?.find(entry => entry.name === current && entry.context?.cluster && entry.context?.user) if (!entry) throw 'current context not found in kubectl config.' const clusterEntry = clusters?.find(clusterItem => clusterItem.name === entry.context?.cluster && clusterItem.cluster?.server) if (!clusterEntry) throw `cluster ${bold(entry.cluster)} not found in kubectl config.` const { cluster } = clusterEntry const serverToContext = {} for (const { name, cluster } of clusters) { const contextEntries = contexts.filter(entry => entry.context.cluster === name) // Collect only cluster servers that are used in exactly one context, to clearly identify the context to be used if (contextEntries.length === 1) { serverToContext[cluster.server] = contextEntries[0] } } return { namespace: current.namespace || 'default', context: entry, cluster, serverToContext } })() } const config = await this.#configPromise let contextEntry if (cluster && cluster !== config.cluster.server) { const context = config.serverToContext[cluster] if (context) contextEntry = context else throw `cluster ${bold(cluster)} not found in kubectl configuration` } else { contextEntry = config.context } return { context: contextEntry.name, cluster: cluster || config.cluster.server, namespace: namespace || contextEntry.context.namespace || 'default' } } }