@sap/cds-mtxs
Version:
SAP Cloud Application Programming Model - Multitenancy library
153 lines (125 loc) • 6.16 kB
JavaScript
const cds = require('@sap/cds')
const { fs, path } = cds.utils
const { token, fetchResiliently } = require('../../../lib/utils')
const { t0 = 't0' } = cds.requires.multitenancy ?? {}
/* API */
class AbstractContainerManagerClient {
constructor({ clientid, clientsecret, url }) {
this.clientid = clientid
this.clientsecret = clientsecret
this.url = url
this.failures = new Map()
this.version = JSON.parse(fs.readFileSync(path.join(__dirname, '../../../package.json'), 'utf8')).version
this.cachedToken = null
this.sm_url = null
}
create = () => {
throw new Error('Method not implemented.')
}
get = async () => {
throw new Error('Method not implemented.')
}
getAll = async () => {
throw new Error('Method not implemented.')
}
acquire = async (tenant, parameters) => {
try { return await this.get(tenant, { disableCache: true }) } catch (e) {
if (e.status === 404) return this.create(tenant, parameters)
throw e
}
}
deploy = (container, tenant, out, options, deployEnv) => {
return require('./hdi').deploy(container, tenant, out, options, deployEnv)
}
remove = async () => {
throw new Error('Method not implemented.')
}
_token = async () => {
if (this.cachedToken && this.cachedToken.expiry >= Date.now() + 30_000) {
return `Bearer ${this.cachedToken.access_token}`
}
const raw = await token(this)
const { access_token, expires_in } = JSON.parse(raw)
this.cachedToken = { access_token, expiry: Date.now() + expires_in * 1000 }
return `Bearer ${access_token}`
}
fetchApi = async (url, conf, { failures = 0, retryUntil } = {}) => {
const { version } = JSON.parse(fs.readFileSync(path.join(__dirname, '../../../package.json'), 'utf8'))
conf.headers ??= {}
conf.headers.Authorization ??= await this._token()
conf.headers['Accept'] ??= 'application/json'
conf.headers['Content-Type'] ??= 'application/json'
conf.headers['Client-ID'] ??= 'cap-mtx-sidecar'
conf.headers['Client-Name'] ??= 'cap-mtx-sidecar'
conf.headers['Client-Version'] ??= version
conf.headers['X-CorrelationID'] ??= cds.context?.id
conf.headers['X-Correlation-ID'] ??= cds.context?.id
conf.baseURL ??= url.startsWith('http') ? '' : this.sm_url + '/v1/'
const { retries, maxRetryAfter } = cds.requires?.multitenancy?.serviceManager ?? cds.requires?.multitenancy?.containerManager ?? {}
return fetchResiliently(conf.baseURL + url, conf, { retries: retries ?? 10, maxRetryAfter: maxRetryAfter ?? 5000, retryUntil, failures })
}
_poll = async (location, conf = {}) => {
let attempts = 0, maxAttempts = 60, pollingTimeout = 3000, maxTime = pollingTimeout * maxAttempts / 1000
const url = location.includes('/v1/') ? location.slice('/v1/'.length) : location
const _next = async (resolve, reject) => {
let response
try {
response = await this.fetchApi(url, conf)
} catch (err) {
return reject(err)
}
if (this._succeeded(response)) return resolve(response)
if (this._failed(response)) return reject(this._pollError(response))
if (attempts > maxAttempts) return reject(new Error(`Polling ${location} timed out after ${maxTime} seconds with state ${response.data?.state ?? 'unknown'}`))
setTimeout(++attempts && _next, pollingTimeout, resolve, reject)
}
return new Promise(_next)
}
_succeeded = (response) => response.data?.state === 'succeeded'
_failed = (response) => response.data?.state === 'failed'
_pollError = (response) => response.data.errors[0] ?? response.data.errors
static _errorMessage = (e, action, tenant) => {
const msg = `Error ${action} tenant ${tenant}: ${e.response?.data?.error ?? e.code ?? e.message ?? 'unknown error'}`
const cause = e.description || e.cause ? require('os').EOL + `Root Cause: ${e.description ?? e.cause}` : ''
return msg + cause
}
_createParams = () => {
throw new Error('Method not implemented.')
}
// collect all hdi parameters
_hdiParams = (tenant, params = {}, metadata) => {
const createParamsFromEnv = cds.env.requires['cds.xt.DeploymentService']?.hdi?.create ?? {}
const createParamsFromTenantOptions = cds.env.requires['cds.xt.DeploymentService']?.for?.[tenant]?.hdi?.create ?? {}
const createParams = { ...createParamsFromEnv, ...createParamsFromTenantOptions, ...params?.hdi?.create }
// @sap/instance-manager API compat
this._checkLegacyConfig(createParams)
// flatter @sap/cds-mtxs config
const bindParamsFromEnv = cds.env.requires['cds.xt.DeploymentService']?.hdi?.bind ?? {}
const bindParamsFromTenantOptions = cds.env.requires['cds.xt.DeploymentService']?.for?.[tenant]?.hdi?.bind ?? {}
const bindParams = { ...bindParamsFromEnv, ...bindParamsFromTenantOptions, ...params?.hdi?.bind }
const final = {}
const provisioningParams = { ...this._encryptionParams(metadata), ...createParams }
if (Object.keys(provisioningParams).length > 0) final.create = provisioningParams
if (Object.keys(bindParams).length > 0) final.bind = bindParams
if (tenant === t0) delete final.create?.dataEncryption
return Object.keys(final).length > 0 ? final : null
}
_encryptionParams = (data) => {
return (data?.globalAccountGUID ?? data?.subscriber?.globalAccountId) ? {
subscriptionContext: {
// crmId: '',
globalAccountID: data.globalAccountGUID ?? data.subscriber.globalAccountId,
subAccountID: data.subscribedSubaccountId ?? data.subscriber.subaccountId,
applicationName: data.subscriptionAppName ?? data.rootApplication?.appName
}
} : {}
}
_checkLegacyConfig = (createParams) => {
const compat = 'provisioning_parameters' in createParams || 'binding_parameters' in createParams
if (compat) {
const capire = 'https://cap.cloud.sap/docs/guides/multitenancy/mtxs#deployment-config'
cds.error(`Warning: Legacy configuration for deployment parameters is used. Please update to the new, simplified deployment options. See ${capire} for details.`)
}
}
}
module.exports = AbstractContainerManagerClient