@sap/cds-mtxs
Advanced tools
Comparing version 1.8.4 to 1.8.5
@@ -32,3 +32,2 @@ #!/usr/bin/env node | ||
} | ||
console.log(`cds-mtx ${cmd} successful.`) | ||
} | ||
@@ -35,0 +34,0 @@ |
@@ -39,3 +39,4 @@ const path = require('path') | ||
} catch (error) { | ||
if (!error.code === 'MODEL_NOT_FOUND') throw error | ||
if (error.code !== 'MODEL_NOT_FOUND') throw error | ||
// ignore extensions with no model | ||
continue | ||
@@ -83,3 +84,7 @@ } | ||
} | ||
// add compiler settings from main | ||
packageJson.cds.cdsc = cds.env.cdsc | ||
await fs.writeFile(path.join(dir, 'package.json'), JSON.stringify(packageJson, 2)) | ||
} |
@@ -120,2 +120,30 @@ const path = require('path') | ||
module.exports.checkMigration = async function checkMigration(req) { | ||
const { tenant } = req.data | ||
if (await mtxAdapter.hasExtensions(tenant)) { | ||
if (module.exports._hasExtensibilityEnv()) { | ||
if (!await mtxAdapter.isMigrated(tenant)) | ||
req.reject(422, `Upgrade of tenant ${tenant} aborted. Extensions have not been migrated yet`) | ||
} else { | ||
req.reject(422, `Upgrade of tenant ${tenant} aborted. Old MTX Extensions exist but extensibility is not configured (cds.requires.extensibility is false)`) | ||
} | ||
} | ||
} | ||
module.exports.getMissingMtxTenants = async (existingTenants) => { | ||
if (await mtxAdapter.wasOldMtx()) { | ||
// TODO do this only once | ||
// get all mtx tenants | ||
const mtxTenants = await mtxAdapter.getAllTenantIds() | ||
// add metadata for non-existing entries | ||
return await Promise.all(mtxTenants.filter( mtxTenant => !existingTenants.includes(mtxTenant)).map( async mtxTenant => { | ||
await module.exports.addMetadata(mtxTenant, { | ||
subscribedTenantId: mtxTenant | ||
}) | ||
return mtxTenant | ||
})) | ||
} | ||
return [] | ||
} | ||
module.exports.migrate = async function migrate(tenants, options) { | ||
@@ -153,3 +181,3 @@ | ||
const migrated = await mtxAdapter.isMigrated(tenant) | ||
if (migrated.timestamp && !force) { | ||
if (migrated?.timestamp && !force) { | ||
migrationResult.log(tenant, `Tenant ${tenant} is already migrated. Skipping migration.`) | ||
@@ -182,3 +210,3 @@ continue | ||
// add metadata in case it had not been added before | ||
await _addMetadata(tenant, metadata) | ||
await module.exports.addMetadata(tenant, metadata) | ||
} catch (error) { | ||
@@ -217,3 +245,3 @@ LOG.log('cds.xt.Extensions not yet deployed, deploying ...') | ||
} catch (error) { | ||
migrationResult.error(tenant, `Extension verification failed for tenant ${tenant} [${tenantProjectFolder}]), skipping migration`, error) | ||
migrationResult.error(tenant, `Extension verification failed for tenant ${tenant} [${tenantProjectFolder}]), skipping migration.`, error) | ||
continue | ||
@@ -244,4 +272,7 @@ } | ||
} else { | ||
// check if extensions exist -> abort if yes | ||
if (await mtxAdapter.hasExtensions(tenant)) throw new Error(`Extensions exist but extensibility is not configured (cds.requires.extensibilty is false)`) | ||
if (!dry) { | ||
await _addMetadata(tenant, metadata) | ||
await module.exports.addMetadata(tenant, metadata) | ||
migrationResult.log(tenant, `Metadata for tenant ${tenant} added.`) | ||
@@ -265,3 +296,3 @@ } | ||
async function _addMetadata(tenant, metadata) { | ||
module.exports.addMetadata = async function addMetadata(tenant, metadata) { | ||
const t0 = getT0() | ||
@@ -285,4 +316,3 @@ // TODO Upsert ? | ||
const mp = await cds.connect.to('cds.xt.ModelProviderService') | ||
const isExtended = await (async () => { try { return await mp.isExtended(tenant) } catch(error) { return false } })() | ||
const mainCsn = await mp.getCsn({ tenant, flavor: 'inferred', activated: true }) | ||
const mainCsn = await mp.getCsn({ tenant, flavor: 'inferred', activated: true, base: true }) | ||
@@ -297,5 +327,3 @@ if (!mainCsn) throw new Error(`Verification error for tenant ${tenant}: Empty base model`) | ||
const extensionCsn = JSON.parse(extensionCsnString) | ||
// do not apply extensions if mp.getCsn already contained extensions | ||
// TODO refine check | ||
if (!isExtended) previewCsn = cds.extend(previewCsn).with(extensionCsn) | ||
previewCsn = cds.extend(previewCsn).with(extensionCsn) | ||
} | ||
@@ -309,6 +337,8 @@ | ||
const diffMessages = [] | ||
// are artifacts lost? | ||
const hanaDiffNewToOld = cds.compiler.to.hdi.migration(cds.minify(previewCsn), {}, cds.minify(existingHana.afterImage)) | ||
if (hanaDiffNewToOld.deletions.length) { | ||
throw new Error(`Verification error for tenant ${tenant}: migrated model is missing artifacts:\n ${hanaDiffNewToOld.deletions.map( ({ name, suffix }) => `${name}${suffix}\n`)}`) | ||
diffMessages.push(`Migrated model is missing artifacts:\n ${hanaDiffNewToOld.deletions.map( ({ name, suffix }) => `${name}${suffix}\n`)}`) | ||
} | ||
@@ -321,4 +351,4 @@ | ||
if (relevantMigrations.length) throw new Error(`Verification error for tenant ${tenant}: table migrations found\n` + | ||
`${relevantMigrations.map( ({ name, suffix, changeset }) => `${name}${suffix}: ${changeset.map(({sql}) => sql)}\n`)}`) | ||
if (relevantMigrations.length) diffMessages.push(`Table migrations found\n` + | ||
`${relevantMigrations.map( ({ name, suffix, changeset }) => ` ${name}${suffix}: ${changeset.map(({sql}) => sql)}\n`)}`) | ||
} | ||
@@ -331,4 +361,7 @@ | ||
if (filteredDeletions.length) { | ||
diffMessages.push(`Migrated model has additional artifacts:\n ${hanaDiffOldToNew.deletions.map( ({ name, suffix }) => `${name}${suffix}\n`)}`) | ||
throw new Error(`Verification error for tenant ${tenant}: migrated model has additional artifacts:\n ${hanaDiffOldToNew.deletions.map( ({ name, suffix }) => `${name}${suffix}\n`)}`) | ||
} | ||
if (diffMessages.length) throw new Error(`Verification error for tenant ${tenant}:\n${diffMessages.join('\n')}`) | ||
} |
@@ -15,3 +15,3 @@ const cds = require('@sap/cds') | ||
const GLOBAL_DATA_META_TENANT = '__META__' | ||
function _tenantFilter(tenantId) { | ||
function _tenantFilter(tenantId, index, allTenants) { | ||
const t0 = cds.env.requires.multitenancy?.t0 ?? 't0' | ||
@@ -23,2 +23,3 @@ return tenantId | ||
&& tenantId !== t0 | ||
&& allTenants.includes(_prefixTenant(tenantId)) | ||
} | ||
@@ -51,7 +52,12 @@ | ||
const result = (await cds.tx({ tenant: _prefixTenant(tenant) }, tx => tx.run(CONTENT_METADATA_QUERY, [type, domain]))) | ||
const { CONTENT } = result && result[0] || {} | ||
const metadata = CONTENT ? JSON.parse(CONTENT) : {} | ||
// more robust behaviour | ||
if (!metadata.subscribedTenantId) metadata.subscribedTenantId = tenant | ||
return metadata | ||
const CONTENT = result?.[0]?.CONTENT || result?.[0]?.content // small tribute to sqlite for testing | ||
if (type === 'onboarding') { | ||
const metadata = CONTENT ? JSON.parse(CONTENT) : {} | ||
// more robust behaviour | ||
if (!metadata.subscribedTenantId) metadata.subscribedTenantId = tenant | ||
return metadata | ||
} else { | ||
return CONTENT ? JSON.parse(CONTENT) : null | ||
} | ||
} catch (error) { | ||
@@ -133,3 +139,3 @@ LOG.error(`Failed to query metadata for tenant '${tenant}' (domain: ${domain}): ${error}`) | ||
} catch (error) { | ||
this.logger.error(`Failed to query extensions for tenant '${tenant}' (domain: ${domain}): ${error}`); | ||
LOG.error(`Failed to query extensions for tenant '${tenant}' (domain: ${domain}): ${error}`); | ||
throw error; | ||
@@ -147,15 +153,33 @@ } | ||
module.exports.verifyMigration = async (tenant) => { | ||
if (!module.exports.wasOldMtx()) return true | ||
return module.exports.isMigrated(tenant) | ||
module.exports.hasExtensions = async (tenant) => { | ||
if (!(await module.exports.wasOldMtx())) return false | ||
const MODEL_FILE_QUERY = 'SELECT FILENAME FROM TENANT_FILES WHERE TYPE = ? AND DOMAIN = ?'; | ||
const domain = cds.env.mtx && cds.env.mtx.domain || '__default__' | ||
try { | ||
const files = (await cds.tx({ tenant: _prefixTenant(tenant) }, tx => tx.run(MODEL_FILE_QUERY, ['extension', domain]))); | ||
return files.length | ||
} catch (error) { | ||
LOG.error(`Failed to query extensions for tenant '${tenant}' (domain: ${domain}): ${error}`); | ||
return false; | ||
} | ||
} | ||
const wasOldMtx = [] | ||
// use this before automatic tenant list update in upgrade | ||
module.exports.wasOldMtx = async () => { | ||
const hana = require('@sap/cds-mtxs/srv/plugins/hana/srv-mgr') | ||
try { | ||
return !!(await hana.get('__META__')) | ||
} catch (error) { | ||
if (error.status === 404) return false | ||
else throw e | ||
if (!wasOldMtx.length) { | ||
if (cds.env.requires.db?.kind === 'hana') { | ||
const hana = require('@sap/cds-mtxs/srv/plugins/hana/srv-mgr') | ||
try { | ||
wasOldMtx.push(!!(await hana.get('__META__'))) | ||
} catch (error) { | ||
if (error.status === 404) wasOldMtx.push(false) | ||
else throw error | ||
} | ||
} else { | ||
wasOldMtx.push(false) | ||
} | ||
} | ||
return wasOldMtx[0] | ||
} | ||
@@ -162,0 +186,0 @@ |
{ | ||
"name": "@sap/cds-mtxs", | ||
"version": "1.8.4", | ||
"version": "1.8.5", | ||
"description": "SAP Cloud Application Programming Model - Multitenancy library", | ||
@@ -16,3 +16,4 @@ "homepage": "https://cap.cloud.sap/", | ||
"db/", | ||
"env.js" | ||
"env.js", | ||
"CHANGELOG.md" | ||
], | ||
@@ -27,6 +28,3 @@ "bin": { | ||
"@sap/hdi-deploy": "^4" | ||
}, | ||
"peerDependencies": { | ||
"@sap/cds": ">=6" | ||
} | ||
} |
const cds = require ('@sap/cds/lib') | ||
const DEBUG = cds.debug('mtx') | ||
class MTXServices extends cds.Service { async init(){ | ||
if (cds.mtx) { | ||
DEBUG?.('bootstrapping old MTX...') | ||
await cds.mtx.in (cds.app) // old mtxExtension | ||
if (!process.env.MTX_MIGRATION) return | ||
} | ||
// else... | ||
class MTXServices extends cds.Service { init(){ | ||
DEBUG?.('bootstrapping MTX services...') | ||
let { definitions } = cds.model | ||
let models = [] | ||
const { definitions } = cds.model | ||
const models = [] | ||
@@ -33,3 +27,3 @@ if (cds.requires.multitenancy) { | ||
if (cds.requires[service] === false) return | ||
else models.push(cds.requires.kinds[service].model) | ||
models.push(cds.requires.kinds[service].model) | ||
} | ||
@@ -36,0 +30,0 @@ |
const cds = require('@sap/cds/lib') | ||
const LOG = cds.log('mtx'), DEBUG = cds.debug('mtx') | ||
const { parseHeaders, sendResult } = require('./saas-registry-util') | ||
const migration = require('../../lib/migration/migration') | ||
const DeploymentService = 'cds.xt.DeploymentService' | ||
@@ -55,3 +56,3 @@ const JobsService = 'cds.xt.JobsService' | ||
const { subscribedTenantId } = data ?? {} | ||
return params?.[0]?.subscribedTenantId ?? subscribedTenantId | ||
return subscribedTenantId ?? params?.[0]?.subscribedTenantId | ||
} | ||
@@ -101,15 +102,21 @@ | ||
if (!one) cds.error(`Tenant ${tenant} not found`, { status: 404 }) | ||
return JSON.parse(one.metadata ?? '{}') | ||
return { subscribedTenantId: tenant, ...JSON.parse(one.metadata ?? '{}') } | ||
} | ||
return (await cds.tx({ tenant: t0 }, tx => | ||
tx.run(SELECT.from(Tenants, tenant => { tenant.ID, tenant.metadata })) | ||
)).map(tenant => JSON.parse(tenant.metadata)) | ||
)).map(({ ID, metadata }) => ({ subscribedTenantId: ID, ...JSON.parse(metadata) })) | ||
} | ||
async upgrade(tenantsIds) { | ||
async _getTenants() { | ||
const tenants = (await cds.tx({ tenant: t0 }, tx => | ||
tx.run(SELECT.from(Tenants, tenant => { tenant.ID })) | ||
)).map(({ ID }) => ID) | ||
const mtxTenants = await migration.getMissingMtxTenants(tenants) | ||
return [...tenants, ...mtxTenants] | ||
} | ||
async upgrade(tenantsIds, options) { | ||
if (!tenantsIds?.length) return | ||
const tenantList = tenantsIds.includes('*') ? undefined : tenantsIds | ||
const tenants = tenantList ?? (await cds.tx({ tenant: t0 }, tx => | ||
tx.run(SELECT.from(Tenants, tenant => { tenant.ID })) | ||
)).map(({ ID }) => ID) | ||
const tenants = tenantList ?? await this._getTenants() | ||
const { isSync } = parseHeaders(cds.context.http?.req.headers) | ||
@@ -126,3 +133,3 @@ const { | ||
await this.limiter(clusterSize, dbToTenants, tenants => | ||
this.limiter(workerSize ?? poolSize, Array.from(tenants), t => ds.tx({tenant:t}, tx => tx.upgrade(t))) | ||
this.limiter(workerSize ?? poolSize, Array.from(tenants), t => ds.tx({tenant:t}, tx => tx.upgrade(t, options))) | ||
) | ||
@@ -138,3 +145,3 @@ await this._sendCallback('SUCCEEDED', 'Tenant upgrade succeeded') | ||
// REVISIT: use jobs service for sync and async operations (might also be interesting for concurrency control) | ||
return tx.enqueue(dbToTenants, 'upgrade', [], error => { | ||
return tx.enqueue(dbToTenants, 'upgrade', [options], error => { | ||
if (error) this._sendCallback('FAILED', 'Tenant upgrade failed') | ||
@@ -141,0 +148,0 @@ else this._sendCallback('SUCCEEDED', 'Tenant upgrade succeeded') |
@@ -0,1 +1,2 @@ | ||
const https = require('https') | ||
const { URL } = require('url') | ||
@@ -5,2 +6,3 @@ const axios = require('axios') | ||
const LOG = cds.log('mtx'), DEBUG = cds.debug('mtx') | ||
require('../../lib/pruneAxiosErrors') | ||
@@ -52,4 +54,6 @@ module.exports = new class SaasRegistryUtil { | ||
const { multitenancy, 'cds.xt.SaasProvisioningService': sps } = cds.env.requires | ||
const { clientid, clientsecret, url } = multitenancy?.credentials ?? sps?.credentials ?? {} | ||
if (!clientid || !clientsecret || !url) { | ||
const { clientid, clientsecret, certurl, url, certificate, key } = multitenancy?.credentials ?? sps?.credentials ?? {} | ||
const auth = certificate ? { maxRedirects: 0, httpsAgent: new https.Agent({ cert: certificate, key }) } | ||
: { auth: { username: clientid, password: clientsecret } } | ||
if (!clientid) { | ||
cds.error('No saas-registry credentials available from the application environment.', { status: 401 }) | ||
@@ -59,16 +63,11 @@ } | ||
try { | ||
LOG.info(`getting saas-registry auth token from ${url}`) | ||
const { data: { access_token } } = await axios(`${url}/oauth/token`, { | ||
const authUrl = `${certurl ?? url}/oauth/token` | ||
LOG.info(`getting saas-registry auth token from ${authUrl}`) | ||
const { data: { access_token } } = await axios(authUrl, { | ||
method: 'POST', | ||
auth: { | ||
username: clientid, | ||
password: clientsecret, | ||
}, | ||
headers: { | ||
'Content-Type': 'application/json' | ||
}, | ||
...auth, | ||
params: { | ||
grant_type: 'client_credentials', | ||
response_type: 'token' | ||
}, | ||
} | ||
}) | ||
@@ -80,4 +79,4 @@ if (!access_token) { | ||
} catch (error) { | ||
cds.error('Could not get auth token for saas-registry: ' + error.message, { status: 401 }) // REVISIT: Just throw error? | ||
cds.error('Could not get auth token for saas-registry: ' + error.message, { status: 401 }) | ||
} | ||
} |
@@ -33,3 +33,8 @@ const cds = require('@sap/cds/lib') | ||
cds.on('served', () => { if (cds.app) cds.app.get('/-/cds/login/token', token) }) | ||
cds.on('served', () => { | ||
if (cds.app) { | ||
cds.app.post('/-/cds/login/token', token) | ||
cds.app.get('/-/cds/login/token', token) | ||
} | ||
}) | ||
@@ -36,0 +41,0 @@ const { 'cds.xt.ModelProviderService': mps } = cds.services |
@@ -18,16 +18,17 @@ const cds = require('@sap/cds/lib') | ||
if (!fqn) cds.error(`Bad handler name ${entityName}-${registration}-${operation}`, { code: 400 }) | ||
if (!extCsn.extensions) extCsn.extensions = [] | ||
const fqName = bound ? entityName : fqn.name | ||
const ext = extCsn.extensions.find(element => element.annotate === fqName) | ||
if (bound) { | ||
if (ext && ext.actions && ext.actions[operation] && ext.actions[operation][CODE_ANNOTATION]) { | ||
const ext = extCsn.extensions.find(element => element.annotate === fqName && element.actions?.[operation]?.[CODE_ANNOTATION]) | ||
if (ext) { | ||
ext.actions[operation][CODE_ANNOTATION].push({ [registration]: operation, code }) | ||
} else { | ||
extCsn.extensions.push({ | ||
annotate: fqName, | ||
extCsn.extensions.push({ | ||
annotate: fqName, | ||
actions: { [operation] : { [CODE_ANNOTATION]: [{ [registration]: operation, code }] } } }) | ||
} | ||
} else { | ||
if (ext && ext[CODE_ANNOTATION]) { | ||
const ext = extCsn.extensions.find(element => element.annotate === fqName && element[CODE_ANNOTATION]) | ||
if (ext) { | ||
ext[CODE_ANNOTATION].push({ [registration]: operation, code }) | ||
@@ -42,4 +43,4 @@ } else { | ||
const handlers = await readHandlers(projectPath) | ||
for (var entry of handlers.entries()) { | ||
await _addAnnotationProd(extCsn, entry[0], entry[1], tenant) | ||
for (var entry of handlers.entries()) { | ||
await _addAnnotationProd(extCsn, entry[0], entry[1], tenant) | ||
} | ||
@@ -46,0 +47,0 @@ } |
const cds = require('@sap/cds/lib'), { fs, path, tar, rimraf } = cds.utils | ||
const linter = require('./linter') | ||
const main = require('../config') | ||
const activate = require('./activate') | ||
@@ -15,6 +17,5 @@ const { getCompilerError } = require('../../lib/utils') | ||
try { | ||
const { extCsn, bundles, csvs } = await readData(extension, root) | ||
const { extCsn, bundles, csvs } = await readData(extension, root) | ||
// REVISIT: adapt flags for side-car: use main | ||
if (cds.env.requires.extensibility?.code) await addCodeAnnotations(root, extCsn, req.tenant) | ||
if (main.requires.extensibility?.code) await addCodeAnnotations(root, extCsn, req.tenant) | ||
@@ -21,0 +22,0 @@ return { extCsn, bundles, csvs } |
@@ -28,4 +28,2 @@ const { URL } = require('url'); | ||
} | ||
assertDefined('query', query); | ||
} | ||
@@ -32,0 +30,0 @@ |
const ClientCredentialsAuthProvider = require('./ClientCredentialsAuthProvider'); | ||
const PasswordAuthProvider = require('./PasswordAuthProvider'); | ||
const RefreshTokenAuthProvider = require('./RefreshTokenAuthProvider'); | ||
const { assertDefined } = require('./util/SecretsUtil'); | ||
module.exports = class AuthProviderFactory { | ||
/** | ||
* Validates all data relevant to the determination of the grant type. | ||
*/ | ||
static #validate(credentials, query) { | ||
assertDefined('query', query); | ||
assertDefined('presence of refresh token or passcode or clientid', !!query.refresh_token || !!query.passcode || !!query.clientid); | ||
} | ||
static getAuthProvider(credentials, query) { | ||
AuthProviderFactory.#validate(credentials, query); | ||
return query.refresh_token | ||
@@ -12,6 +22,4 @@ ? new RefreshTokenAuthProvider(credentials, query) | ||
? new PasswordAuthProvider(credentials, query) | ||
: query.clientid | ||
? new ClientCredentialsAuthProvider(credentials, query) | ||
: new Error('Unknown grant type'); | ||
: new ClientCredentialsAuthProvider(credentials, query); | ||
} | ||
} |
@@ -1,2 +0,1 @@ | ||
const util = require('util'); | ||
const cds = require('@sap/cds/lib'); | ||
@@ -6,4 +5,21 @@ const { getAuthProvider } = require('./authProvider/AuthProviderFactory'); | ||
const LOG = cds.log('mtx'); | ||
const DEBUG = cds.debug('mtx'); | ||
const DEBUG = cds.debug('req|mtx'); | ||
require('../../../lib/pruneAxiosErrors'); | ||
async function parseBody(request) { | ||
return new Promise((resolve, reject) => { | ||
const chunks = []; | ||
request.on('data', chunk => chunks.push(chunk)); | ||
request.on('end', () => { | ||
try { | ||
const body = Buffer.concat(chunks).toString(); | ||
request.body = Object.fromEntries(new URLSearchParams(body).entries()); | ||
resolve(request.body); | ||
} catch (error) { | ||
reject(error); | ||
} | ||
}); | ||
}); | ||
} | ||
module.exports = async function token(request, response) { | ||
@@ -17,3 +33,5 @@ if (request.method === 'HEAD') { | ||
const { credentials } = cds.env.requires.auth; | ||
const { query } = request; | ||
const query = request.method === 'POST' | ||
? await parseBody(request) | ||
: request.query; | ||
@@ -34,3 +52,3 @@ let authProvider; | ||
...authProvider.clientAuth, | ||
timeout: 10000 | ||
timeout: 1e4 // ms | ||
} | ||
@@ -41,22 +59,12 @@ ); | ||
} catch (axError) { | ||
const code = axError.code ? `${axError.code}. ` : ''; | ||
const data = axError.response?.data; | ||
const reason = data?.error /* RFC 6749 */ ?? axError.message; | ||
const details = (axError.response?.status === 401 ? `Client authentication: ${authProvider.clientAuthToLog()}. ` : '') + | ||
`POST data: '${authProvider.postDataToLog()}'. `; | ||
const passcodeUrl = new URL(authProvider.authUrl); | ||
passcodeUrl.pathname = '/passcode'; | ||
let details = ''; | ||
if (data) { | ||
details += `Details: '${data.error_description /* RFC 6749 */ || util.inspect(data)}'. `; | ||
} | ||
if (axError.response?.status === 401) { | ||
details += `Client authentication: ${authProvider.clientAuthToLog()}. `; | ||
} | ||
details += `POST data: '${authProvider.postDataToLog()}'. `; | ||
const toLog = `Authentication failed: ${axError.message} ${details}Passcode URL: ${passcodeUrl}`; | ||
const toSend = `Authentication failed: ${axError.message}. Passcode URL: ${passcodeUrl}`; | ||
const toLog = `Authentication failed: ${code}${reason}. ${details}Passcode URL: ${passcodeUrl}`; | ||
const toSend = `Authentication failed: ${code}${reason}. Passcode URL: ${passcodeUrl}`; | ||
const status = axError.status ?? 500; | ||
const status = axError.response?.status ?? 500; | ||
LOG.error(toLog); | ||
@@ -63,0 +71,0 @@ DEBUG && LOG.error(axError); |
@@ -5,3 +5,2 @@ const cds = require('@sap/cds/lib'), { uuid } = cds.utils | ||
const DeploymentService = 'cds.xt.DeploymentService' | ||
const Jobs = 'cds.xt.Jobs', Tasks = 'cds.xt.Tasks' | ||
@@ -29,10 +28,15 @@ | ||
await retry(() => cds.tx({ tenant: t0 }, tx => tx.run(INSERT.into(Tasks, tasks))), LOG) | ||
if (tasks.length) { | ||
await retry(() => cds.tx({ tenant: t0 }, tx => tx.run(INSERT.into(Tasks, tasks))), LOG) | ||
const ds = await cds.connect.to(DeploymentService) | ||
const tx = ds.tx(cds.context) | ||
_nextJob(jobs, task => { | ||
const { 'cds.xt.DeploymentService': ds } = cds.services | ||
return ds.tx({ tenant: cds.context.tenant }, tx => tx[op](task.tenant, ...args)) | ||
}, onJobDone).catch(err => LOG.error('next job raised an error', err)) | ||
} else { | ||
await retry(() => cds.tx({ tenant: t0 }, tx => | ||
tx.run(UPDATE(Jobs, { ID: job_ID }).with({ status: FINISHED })) | ||
), LOG) | ||
} | ||
_nextJob(jobs, task => tx[op](task.tenant, ...args), onJobDone) | ||
.catch(err => LOG.error('next job raised an error', err)) | ||
const url = process.env.VCAP_APPLICATION ? 'https://' + JSON.parse(process.env.VCAP_APPLICATION).uris?.[0] : cds.server.url | ||
@@ -39,0 +43,0 @@ cds.context.http?.res.set('Location', `${url}/-/cds/jobs/pollJob(ID='${job_ID}')`) |
@@ -7,12 +7,4 @@ const fs = require('fs').promises | ||
const TEMP_DIR = require('fs').realpathSync(require('os').tmpdir()) | ||
const main = conf.root ? new class { //> we're running in sidecar -> use env of main app | ||
get env() { return super.env = cds.env.for ('cds', this.root) } | ||
get root() { return super.root = path.resolve (cds.root, conf.root) } | ||
get requires() { return super.requires = this.env.requires } | ||
cache = {} //> for cds.resolve() | ||
} : { //> not in sidecar | ||
requires: cds.requires, | ||
root: cds.root, | ||
env: cds.env, | ||
} | ||
const main = require('./config') | ||
// If we run in sidecar with mocked auth, use the main app's configured mock users | ||
@@ -123,3 +115,3 @@ if (conf.root && cds.env.requires.auth?.users) { | ||
const extensions = !base && await _getExtensions4 (tenant, activated) | ||
const extensions = !base && main.requires.extensibility && await _getExtensions4 (tenant, activated) | ||
if (!extensions && checkExt) req.reject(404, 'Missing extensions') | ||
@@ -199,15 +191,11 @@ | ||
async function getExtResources(req) { | ||
const tenant = (req.user.is('internal-user') && req.data.tenant) || req.tenant | ||
if (tenant) cds.context = { tenant } | ||
const tenant = req.data.tenant || req.tenant | ||
try { | ||
async function _getExtResources(tx) { | ||
const cqn = SELECT('sources').from('cds.xt.Extensions').where('sources !=', null).orderBy('timestamp') | ||
const extSources = await cds.db.run(cqn) | ||
const extSources = await tx.run(cqn) | ||
if (extSources && extSources.length) { | ||
const root = await fs.mkdtemp(`${TEMP_DIR}${path.sep}extension-`) | ||
try { | ||
for (let result of extSources) { | ||
await readData(result.sources, root) | ||
} | ||
// repack all extension content | ||
await Promise.all(extSources.map(({sources}) => readData(sources, root))) | ||
return await tar.cz (root) // REVISIT: pipe to res instead of materializing buffer | ||
@@ -218,2 +206,9 @@ } finally { | ||
} | ||
} | ||
try { | ||
if (tenant) cds.context = { tenant } | ||
return await _getExtResources(cds.db) | ||
// if (cds.context.tenant === tenant) return await _getExtResources(cds.db) | ||
// else return await cds.db.tx({ tenant: tenant}, _getExtResources) | ||
} catch (e) { | ||
@@ -223,4 +218,2 @@ DEBUG?.('cds.xt.Extensions not yet deployed', e) // REVISIT: Questionable usage of try-catch pattern | ||
} | ||
return null | ||
} | ||
@@ -227,0 +220,0 @@ } |
@@ -18,2 +18,4 @@ const cds = require('@sap/cds/lib'), {db} = cds.env.requires | ||
const migration = require('../../lib/migration/migration') | ||
if (db?.kind === 'hana') { | ||
@@ -58,3 +60,8 @@ | ||
}) | ||
// check migration before upgrade | ||
ds.before('upgrade', async (req) => { | ||
await migration.checkMigration(req) | ||
}) | ||
}) | ||
@@ -192,4 +199,5 @@ } | ||
if (updateCsvs) { | ||
const _tabledata4 = require('@sap/cds/bin/build/provider/hana/2tabledata') | ||
const tdata = await _tabledata4 (csn, { dirs: [path.join('gen',tenant,'src','gen','data')] }) | ||
const toHdbtabledata = cds.compile.to.hdbtabledata ?? require(path.join(cds.home, 'bin/build/provider/hana/2tabledata')) // cds@6 compatibility | ||
const tdata = await toHdbtabledata(csn, { dirs: [path.join('gen', tenant, 'src', 'gen', 'data')] }) | ||
for (const [data, { file, csvFolder }] of tdata) { | ||
@@ -204,6 +212,6 @@ gen.push (fs.promises.writeFile(path.join(csvFolder,file), JSON.stringify(data))) | ||
async function _deploy (req, _container) { | ||
const { tenant, options: { _: params, csn: csnFromParameter } = {} } = req.data | ||
const { tenant, options: { _: params = {}, csn: csnFromParameter } = {} } = req.data | ||
// avoid undeploy if csn is passed - would potentially delete all tables | ||
if (csnFromParameter && params?.hdi?.deploy?.auto_undeploy) params.hdi.deploy.auto_undeploy = false | ||
if (csnFromParameter) params.hdi = { ...params.hdi, deploy: { ...params.hdi?.deploy, auto_undeploy: false }} | ||
@@ -215,3 +223,3 @@ if (!cds.db) cds.db = cds.services.db = await cds.connect.to(db) | ||
DEBUG?.('preparing HANA deployment artifacts') | ||
const _resources = ( csnFromParameter ) ? null : resources4(tenant) | ||
const _resources = csnFromParameter ? null : resources4(tenant) | ||
@@ -218,0 +226,0 @@ let container |
const { join } = require('path') | ||
const HdiDeployUtil = require('@sap/cds/bin/deploy/to-hana/hdiDeployUtil') | ||
const { clean_env } = require('@sap/hdi-deploy/library') | ||
const { deploy, clean_env } = require('@sap/hdi-deploy/library') | ||
const cds = require ('@sap/cds/lib') | ||
const LOG = cds.log('mtx|deploy'), DEBUG = cds.debug('mtx|deploy') | ||
const { fs, mkdirp } = cds.utils | ||
exports.deploy = async (hana, tenant, cwd, options) => { | ||
const LOG = cds.log('mtx'), DEBUG = cds.debug('mtx') | ||
const env = _hdi_env4(tenant,hana,options) | ||
@@ -14,7 +13,26 @@ DEBUG?.(`deployment directory: ${cwd}`) | ||
const logPath = join(cds.root, 'logs', `${cds.context.tenant}.log`) | ||
await mkdirp('logs') | ||
const writeStream = fs.createWriteStream(logPath) | ||
await mkdirp('logs') | ||
LOG.info('------------[BEGIN HDI-DEPLOY-OUTPUT]---------------') | ||
try { | ||
await HdiDeployUtil.deployTenant (cwd, env, LOG) | ||
return await new Promise((resolve, reject) => { | ||
deploy(cwd, env, (error, response) => { | ||
if (error) return reject(error) | ||
if (response?.exitCode) { | ||
let message = `HDI deployment failed with exit code ${response.exitCode}` | ||
if (response.signal) message += `. ${response.signal}` | ||
return reject(new Error(message)) | ||
} | ||
return resolve() | ||
}, { | ||
stderrCB: buffer => { | ||
LOG.error(buffer.toString()) | ||
writeStream.write(buffer) | ||
}, | ||
stdoutCB: buffer => { | ||
DEBUG?.(buffer.toString()) | ||
writeStream.write(buffer) | ||
} | ||
}) | ||
}) | ||
} finally { | ||
@@ -61,3 +79,3 @@ LOG.info('-------------[END HDI-DEPLOY-OUTPUT]----------------') | ||
const _parse_env = (key, options) => { | ||
const val = process.env[key]; if (!val) return {} | ||
const val = process.env[key]; if (!val) return { ...options } | ||
try { | ||
@@ -64,0 +82,0 @@ return { ...JSON.parse (val), ...options } |
@@ -8,2 +8,3 @@ const https = require('https') | ||
const axios = require('axios') | ||
require('../../../lib/pruneAxiosErrors') | ||
const api = axios.create({ baseURL: sm_url + '/v1/', 'Content-Type': 'application/json' }) | ||
@@ -29,3 +30,3 @@ api.interceptors.request.use(async config => { | ||
} catch (e) { | ||
if (e.response.status === 409) service_instance_id = (await _instance4(tenant)).id | ||
if (e.response?.status === 409) service_instance_id = (await _instance4(tenant)).id | ||
else cds.error(_errorMessage(e, 'creating', tenant), { status: e.response?.status ?? 500 }) | ||
@@ -148,3 +149,3 @@ } | ||
if (state === 'succeeded') return resolve(resource_id) | ||
if (state === 'failed') return reject(errors[0]) | ||
if (state === 'failed') return reject(errors[0] ?? errors) | ||
if (attempts > 20) return reject(new Error(`Polling ${location} timed out`)) | ||
@@ -158,4 +159,4 @@ setTimeout(++attempts && _next, 3000, resolve, reject) | ||
const msg = `Error ${action} tenant ${tenant}: ${e.response?.data?.error ?? e.code ?? 'unknown error'}` | ||
const cause = `Root Cause: ${e.response?.data?.error_description ?? e.response?.data.description ?? e.response?.data ?? e.cause ?? e}` | ||
return msg + require('os').EOL + cause | ||
const cause = e.cause ? require('os').EOL + `Root Cause: ${e.description ?? e.cause}` : '' | ||
return msg + cause | ||
} | ||
@@ -162,0 +163,0 @@ |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
216390
2
66
4247
3