@sap/cds-mtxs
Advanced tools
Comparing version 1.17.0 to 1.18.0
@@ -10,3 +10,3 @@ #!/usr/bin/env node | ||
async function cds_mtx(cmd, tenant) { | ||
async function cds_mtx(cmd, tenant, body) { | ||
@@ -21,2 +21,11 @@ let connectedTenants = [tenant] | ||
if (/.*,.*/.test(tenant)) return _handleError(`List of tenants not supported: ${tenant}`) | ||
// parse body and handle error | ||
let parsedMetadata | ||
if (body) { | ||
try { | ||
parsedMetadata = JSON.parse(body) | ||
} catch (e) { | ||
return _handleError(`Invalid subscription body: ${e.message}: ${body} `) | ||
} | ||
} | ||
const { 'cds.xt.DeploymentService':ds, 'cds.xt.SaasProvisioningService':sps } = await cds.serve ([ | ||
@@ -34,2 +43,3 @@ '@sap/cds-mtxs/srv/deployment-service', | ||
if (tenant === '*') { | ||
if (cmd !== 'upgrade') return _handleError('"*" only supported for upgrade command') | ||
const tenants = await sps.read('tenant') | ||
@@ -40,6 +50,8 @@ connectedTenants = tenants.map(t => t.subscribedTenantId) | ||
} | ||
await ds[cmd](tenant) | ||
await ds[cmd](tenant, parsedMetadata, parsedMetadata) | ||
} catch(e) { | ||
console.error(e.message) | ||
process.exit(1) | ||
if (isCli) { | ||
console.error(e.message) | ||
process.exit(1) | ||
} else throw e | ||
} finally { | ||
@@ -82,4 +94,4 @@ if (cds.db) { | ||
async function _handleError(message) { | ||
console.error(message) | ||
if (isCli) { | ||
console.error(message) | ||
process.exit(1) | ||
@@ -95,3 +107,3 @@ } | ||
cds-mtx <command> <tenant> | ||
cds-mtx <command> <tenant> [--body <json>] | ||
@@ -103,2 +115,10 @@ COMMANDS | ||
upgrade upgrade a tenant | ||
EXAMPLES | ||
cds-mtx subscribe t1 | ||
cds-mtx subscribe t1 --body '{ "_": { "hdi": { "create": { "database_id": "<database id>" } } } }' | ||
cds-mtx unsubscribe t1 | ||
cds-mtx upgrade t1 | ||
cds-mtx upgrade * | ||
` | ||
@@ -109,5 +129,6 @@ ) | ||
if (isCli) { | ||
const [, , cmd, tenant] = process.argv | ||
;(async () => await cds_mtx(cmd, tenant))() | ||
const [, , cmd, tenant, option, json] = process.argv | ||
if (option && option !== '--body') _usage(`Invalid option ${option}`) | ||
;(async () => await cds_mtx(cmd, tenant, option ? json : undefined))() | ||
} | ||
module.exports = { cds_mtx } |
@@ -9,2 +9,17 @@ # Change Log | ||
## Version 1.18.0 - 2024-04-29 | ||
### Added | ||
- `cds-mtx subscribe <tenant> --body <json>` now allows to pass tenant metadata and HDI parameters. | ||
### Changed | ||
- Retries for failed upgrades are more resilient, using an exponential backoff mechanism and more retries. | ||
### Fixed | ||
- Extension linter is now also called if extensions are created via API. | ||
- The Service Manager credentials cache is correctly invalidated following a resubscription. | ||
## Version 1.17.0 - 2024-03-25 | ||
@@ -11,0 +26,0 @@ |
@@ -25,3 +25,3 @@ const path = require('path') | ||
await fs.rm(projectFolder, { recursive: true, force: true }) | ||
} catch (e) { | ||
} catch (_) { | ||
// ignore | ||
@@ -28,0 +28,0 @@ } |
@@ -408,3 +408,3 @@ const path = require('path') | ||
} catch (e) { | ||
req.reject(404, `No migrated projects found for tenant ${tenant}`) | ||
req.reject(404, `No migrated projects found for tenant ${tenant}: ${e.message}`) | ||
} | ||
@@ -411,0 +411,0 @@ } |
@@ -113,9 +113,9 @@ const LOG = cds.log('mtx') | ||
* @param {number} [retryCount=5] | ||
* @param {number} [retryGap=5000] | ||
* @param {number} [initialRetryGap=5000] | ||
* @returns {Promise<T>} | ||
*/ | ||
const retry = async(fn, retryCount = 5, retryGap = 5 * 1000) => { | ||
const retry = async (fn, retryCount = cds.requires.multitenancy.retries ?? 10, initialRetryGap = 5000) => { | ||
let errorCount = 0 | ||
let finalError | ||
let retryGap = initialRetryGap | ||
while (errorCount < retryCount - 1) { | ||
@@ -131,2 +131,3 @@ try { | ||
await promisify(setTimeout)(retryGap) // eslint-disable-line no-await-in-loop | ||
retryGap *= 1.5 | ||
} | ||
@@ -133,0 +134,0 @@ } |
{ | ||
"name": "@sap/cds-mtxs", | ||
"version": "1.17.0", | ||
"version": "1.18.0", | ||
"description": "SAP Cloud Application Programming Model - Multitenancy library", | ||
@@ -5,0 +5,0 @@ "homepage": "https://cap.cloud.sap/", |
@@ -126,3 +126,3 @@ const cds = require('@sap/cds/lib') | ||
cert = new X509Certificate(buffer) | ||
} catch (e) { | ||
} catch (_) { | ||
return req.reject(401, 'Invalid certificate') | ||
@@ -129,0 +129,0 @@ } |
@@ -7,2 +7,3 @@ const cds = require('@sap/cds/lib') | ||
const activateExt = require('./activate') | ||
const { runLinter } = require('./utils') | ||
@@ -65,3 +66,3 @@ const _isCSN = str => str.substring(0, 1) === '{' | ||
} catch (e) { | ||
req.reject(422, 'Invalid json content in i18n.json') | ||
req.reject(422, `Invalid json content in i18n.json: ${e.message}`) | ||
} | ||
@@ -104,3 +105,4 @@ return | ||
} catch (err) { | ||
req.reject(400, getCompilerError(err.messages)) | ||
if (err.code === 'ERR_CDS_COMPILATION_FAILURE') req.reject(422, getCompilerError(err.messages)) | ||
else req.reject(400, err.message) | ||
} | ||
@@ -157,2 +159,5 @@ } | ||
// call linter | ||
await runLinter(tenant, extCsn, tag, req) | ||
const { bundles, csvs } = _getFiles(resources, req) | ||
@@ -159,0 +164,0 @@ if (tag) await DELETE.from('cds.xt.Extensions').where({ tag }) |
@@ -7,5 +7,4 @@ const cds = require('@sap/cds/lib'), { fs, path, tar, rimraf } = cds.utils | ||
const activate = require('./activate') | ||
const { getCompilerError } = require('../../lib/utils') | ||
const { addCodeAnnotations } = require('./code-extensibility/addCodeAnnotProd') | ||
const { readData } = require('./utils') | ||
const { readData, runLinter } = require('./utils') | ||
@@ -33,3 +32,3 @@ const TEMP_DIR = fs.realpathSync(require('os').tmpdir()) | ||
tenant: req.tenant, | ||
toggles: Object.keys(cds.context.features || {}), // with all enabled feature extensions | ||
toggles: cds.context.features, // with all enabled feature extensions | ||
base: true, // without any custom extensions | ||
@@ -80,21 +79,4 @@ flavor: 'xtended' | ||
// do validation after extension table update - trust transaction handling for rollback | ||
// compiler validation | ||
LOG.info(`validating extension '${tag}' ...`) | ||
const { 'cds.xt.ModelProviderService': mps } = cds.services | ||
// REVISIT: Isn't that also done during activate? | ||
let csn | ||
try { | ||
csn = await mps.getCsn(tenant, Object.keys(cds.context.features || {})) | ||
} catch (err) { | ||
return req.reject(400, getCompilerError(err.messages)) | ||
} | ||
// extension linters | ||
const findings = linter.lint(extCsn, csn) | ||
if (findings.length > 0) { | ||
let message = `Validation for ${tag} failed with ${findings.length} finding(s):\n\n` | ||
message += findings.map(f => ' - ' + f.message).join('\n') + '\n' | ||
return req.reject(422, message) | ||
} | ||
await runLinter(tenant, extCsn, tag, req) | ||
@@ -101,0 +83,0 @@ LOG.info(`activating extension '${tag}' ...`) |
const { path, tar, read, readdir } = cds.utils | ||
const linter = require('./linter') | ||
const LOG = cds.log('mtx') | ||
const { getCompilerError } = require('../../lib/utils') | ||
@@ -27,2 +30,24 @@ const readData = async function (extension, root) { | ||
module.exports = { readData } | ||
const runLinter = async function (tenant, extCsn, tag, req) { | ||
LOG.info(`validating extension '${tag}' ...`) | ||
const { 'cds.xt.ModelProviderService': mps } = cds.services | ||
// REVISIT: Isn't that also done during activate? | ||
let csn | ||
try { | ||
csn = await mps.getCsn(tenant, cds.context.features) | ||
} catch (err) { | ||
return req.reject(400, getCompilerError(err.messages)) | ||
} | ||
const findings = linter.lint(extCsn, csn) | ||
if (findings.length > 0) { | ||
let message = `Validation for ${tag} failed with ${findings.length} finding(s):\n\n` | ||
message += findings.map(f => ' - ' + f.message).join('\n') + '\n' | ||
return req.reject(422, message) | ||
} | ||
} | ||
module.exports = { readData, runLinter } |
@@ -49,6 +49,8 @@ const crypto = require('crypto') | ||
this.before(['getCsn', 'getEdmx', 'getExtCsn', 'getI18n'], req => { | ||
const regex = /^[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)*$|^\$hash$|^\*$/ | ||
const toggles = req.data?.toggles | ||
const invalid = Array.isArray(toggles) ? toggles.find(t => !regex.test(t)) : | ||
typeof toggles === 'string' && !regex.test(toggles) && toggles | ||
let toggles = req.data?.toggles | ||
if (!toggles) return | ||
else if (Array.isArray(toggles)) ; //> go on below... | ||
else if (typeof toggles === 'object') toggles = Object.keys(toggles) | ||
else if (typeof toggles === 'string') toggles = toggles.split(',') | ||
const invalid = toggles.find(t => t !== '*' && !/^[\w-]+$/.test(t)) | ||
if (invalid) return req.reject(400, `Unsupported input toggle param ${invalid}`) | ||
@@ -151,3 +153,3 @@ }) | ||
async function _getCsn (req, checkExt) { | ||
const { tenant, toggles, base, flavor, for:javaornode, activated } = req.data | ||
let { tenant, toggles, base, flavor, for:javaornode, activated } = req.data | ||
@@ -167,2 +169,3 @@ if (conf._in_sidecar) { | ||
if (toggles && typeof toggles === 'object' && !Array.isArray(toggles)) toggles = Object.keys(toggles) | ||
const features = (!toggles || !main.requires.toggles) ? [] : toggles === '*' || toggles.includes('*') ? [fts] : toggles.map (f => fts.replace('*',f)) | ||
@@ -169,0 +172,0 @@ const models = cds.resolve (['*',...features], main); if (!models) return |
const https = require('https') | ||
const { inspect } = require('util') | ||
const cds = require('@sap/cds') | ||
@@ -7,2 +8,3 @@ const LOG = cds.log('mtx'), DEBUG = cds.debug('mtx|sm') | ||
const { sm_url, url, clientid, clientsecret, certurl, certificate, key } = cds.env.requires.db.credentials | ||
const COLORS = !!process.stdout.isTTY && !!process.stderr.isTTY && !process.env.NO_COLOR | ||
const axios = require('axios') | ||
@@ -12,9 +14,14 @@ const api = axios.create({ baseURL: sm_url + '/v1/', headers: { 'Content-Type': 'application/json' }}) | ||
conf.headers.Authorization = await _token() | ||
DEBUG?.(conf.method.toUpperCase(), conf.baseURL + conf.url, { | ||
DEBUG?.('>', conf.method.toUpperCase(), conf.baseURL + conf.url, inspect({ | ||
...(conf.params && { params: conf.params }), | ||
...(conf.data && { data: conf.data }) | ||
}) | ||
}, { depth: 11, compact: false, colors: COLORS })) | ||
return conf | ||
}) | ||
api.interceptors.response.use(response => response, require('../../../lib/pruneAxiosErrors')) | ||
api.interceptors.response.use(response => { | ||
const { method, baseURL, url, status, statusText } = response.config | ||
DEBUG?.('<', method.toUpperCase(), baseURL + url, status, statusText, | ||
inspect(response.data, { depth: 11, colors: COLORS })) | ||
return response | ||
}, require('../../../lib/pruneAxiosErrors')) | ||
@@ -135,5 +142,5 @@ /* API */ | ||
} while (token) | ||
const cacheMisses = Object.fromEntries(fetched.filter(b => b.labels?.tenant_id).map(b => [b.labels.tenant_id[0], b])) | ||
Object.assign(_bindings4.cached, cacheMisses) | ||
if (useCache) { | ||
const cacheMisses = Object.fromEntries(fetched.filter(b => b.labels?.tenant_id).map(b => [b.labels.tenant_id[0], b])) | ||
Object.assign(_bindings4.cached, cacheMisses) | ||
return tenants.map(t => _bindings4.cached[t]) | ||
@@ -140,0 +147,0 @@ } |
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
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
261611
4954
22