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