core/routemgmt/common/utils.js (549 lines of code) (raw):

/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Route management action common utilities */ var request = require('request'); var utils2 = require('./apigw-utils.js'); /** * Register a tenant with the API GW. * A new tenant is created for each unique namespace/tenantinstance. If the * tenant already exists, the tenant is left as-is * Parameters: * gwInfo - Required. API GW connection information (gwUrl, gwAuth) * namespace - Required. Namespace of tenant * tenantInstance - Optional. Tenanant instance used to create >1 tenant per namespace * Defaults to 'openwhisk' * Returns: * tenant object - JSON object representing the tenant in the following format: * { id: GUID, namespace: NAMESPACE, instance: 'openwhisk' } */ function createTenant(gwInfo, namespace, tenantInstance) { var instance = tenantInstance || 'openwhisk'; // Default to a fixed instance so all openwhisk tenants have a common instance var options = { followAllRedirects: true, url: gwInfo.gwUrl+'/tenants', headers: { 'Accept': 'application/json' }, json: { // Auto set header: 'Content-Type': 'application/json' instance: instance, namespace: namespace } }; if (gwInfo.gwAuth) { options.headers.Authorization = 'Basic ' + gwInfo.gwAuth; } console.log('addTenantToGateway: request: '+JSON.stringify(options)); return new Promise(function(resolve, reject) { request.put(options, function(error, response, body) { var statusCode = response ? response.statusCode : undefined; console.log('addTenantToGateway: response status: '+ statusCode); if (error) console.error('Warning: addTenantToGateway request failed: '+utils2.makeJsonString(error)); if (body) console.log('addTenantToGateway: response body: '+utils2.makeJsonString(body)); if (error) { console.error('addTenantToGateway: Unable to configure a tenant on the API Gateway'); reject('Unable to configure the API Gateway: '+utils2.makeJsonString(error)); } else if (statusCode != 200) { if (body) { var errMsg = JSON.stringify(body); if (body.error && body.error.message) errMsg = body.error.message; reject('API Gateway failure (status code '+statusCode+'): '+ errMsg); } else { reject('Unable to configure the API Gateway: Response failure code: '+statusCode); } } else { if (body && body.id) { // body has format like: { id: GUID, namespace: NAMESPACE, instance: 'openwhisk' } console.log('addTenantToGateway: got a single tenant response'); resolve(body); } else { console.error('addTenantToGateway: failure: No tenant guid provided'); reject('Unable to configure the API Gateway: Invalid response from API Gateway'); } } }); }); } /* * Return an array of tenants */ function getTenants(gwInfo, ns, tenantInstance) { var qsNsOnly = { 'filter[where][namespace]' : ns }; var qsNsAndInstance = { 'filter[where][namespace]' : ns, 'filter[where][instance]' : tenantInstance }; var qs = qsNsOnly; if (tenantInstance) qs = qsNsAndInstance; var options = { followAllRedirects: true, url: gwInfo.gwUrl+'/tenants', qs: qs, headers: { 'Accept': 'application/json' }, }; if (gwInfo.gwAuth) { options.headers.Authorization = 'Basic ' + gwInfo.gwAuth; } console.log('getTenants: request: '+JSON.stringify(options)); return new Promise(function(resolve, reject) { request.get(options, function(error, response, body) { var statusCode = response ? response.statusCode : undefined; console.log('getTenants: response status: '+ statusCode); if (error) console.error('Warning: getTenant request failed: '+utils2.makeJsonString(error)); if (body) console.log('getTenants: response body: '+utils2.makeJsonString(body)); if (error) { console.error('getTenants: Unable to obtain tenant from the API Gateway'); reject('Unable to obtain Tenant from the API Gateway: '+utils2.makeJsonString(error)); } else if (statusCode != 200) { if (body) { var errMsg = JSON.stringify(body); if (body.error && body.error.message) errMsg = body.error.message; reject('API Gateway failure (status code '+statusCode+'): '+ errMsg); } else { reject('Unable to configure the API Gateway: Response failure code: '+statusCode); } } else { if (body) { try { var bodyJson = JSON.parse(body); if (Array.isArray(bodyJson)) { resolve(bodyJson); } else { console.error('getTenants: Invalid API GW response body; a JSON array was not returned'); resolve( [] ); } } catch(e) { console.error('getTenants: Invalid API GW response body; JSON.parse() failure: '+e); reject('Internal error. Invalid API Gateway response: '+e); } } else { console.log('getTenants: No tenants found'); resolve( [] ); } } }); }); } /** * Configures an API route on the API Gateway. This API will map to an OpenWhisk action that * will be invoked by the API Gateway when the API route is accessed. * * @param gwInfo Required. * @param gwUrl Required. The base URL gateway path (i.e. 'PROTOCOL://gw.host.domain:PORT/CONTEXT') * @param gwAuth Required. The credentials used to access the API Gateway REST endpoints * @param tenantId Required. * @param swaggerApi Required. The gateway API object to send to the API gateway * @param payload.namespace Required. The OpenWhisk namespace of the user defining this API route * @param payload.gatewayPath Required. The relative path for this route * @param payload.gatewayMethod Required. The gateway route REST verb * @param payload.backendUrl Required. The full REST URL used to invoke the associated action * @param payload.backendMethod Required. The REST verb used to invoke the associated action * @return A promise for an object describing the result with fields error and response */ function addApiToGateway(gwInfo, tenantId, swaggerApi, gwApiId) { var requestFcn = request.post; // Init the GW API configuration object; base it off the swagger API var gwApi; try { gwApi = generateGwApiFromSwaggerApi(swaggerApi); } catch(e) { console.error('generateGwApiFromSwaggerApi exception: '+e); return Promise.reject('Invalid API configuration: '+e); } gwApi.tenantId = tenantId; var options = { followAllRedirects: true, url: gwInfo.gwUrl+'/apis', headers: { 'Accept': 'application/json' }, json: gwApi, // Use of json automaticatlly sets header: 'Content-Type': 'application/json' }; if (gwInfo.gwAuth) { options.headers.Authorization = 'Basic ' + gwInfo.gwAuth; } if (gwApiId) { console.log("addApiToGateway: Updating existing API"); gwApi.id = gwApiId; options.url = gwInfo.gwUrl+'/apis/'+gwApiId; requestFcn = request.put; } console.log('addApiToGateway: request: '+JSON.stringify(options, " ", 2)); return new Promise(function(resolve, reject) { requestFcn(options, function(error, response, body) { var statusCode = response ? response.statusCode : undefined; console.log('addApiToGateway: response status:'+ statusCode); if (error) console.error('Warning: addRouteToGateway request failed: '+ utils2.makeJsonString(error)); if (body) console.log('addApiToGateway: response body: '+utils2.makeJsonString(body)); if (error) { console.error('addApiToGateway: Unable to configure the API Gateway'); reject('Unable to configure the API Gateway: '+utils2.makeJsonString(error)); } else if (statusCode != 200) { if (body) { var errMsg = JSON.stringify(body); if (body.error && body.error.message) errMsg = body.error.message; reject('Unable to configure the API Gateway (status code '+statusCode+'): '+ errMsg); } else { reject('Unable to configure the API Gateway: Response failure code: '+statusCode); } } else if (!body) { console.error('addApiToGateway: Unable to configure the API Gateway: No response body'); reject('Unable to configure the API Gateway: No response received from the API Gateway'); } else { resolve(body); } }); }); } /** * Removes an API route from the API Gateway. * * @param gwInfo Required. * @param gwUrl Required. The base URL gateway path (i.e. 'PROTOCOL://gw.host.domain:PORT/CONTEXT') * @param gwAuth Optional. The credentials used to access the API Gateway REST endpoints * @param apiId Required. Unique Gateway API Id * @return A promise for an object describing the result with fields error and response */ function deleteApiFromGateway(gwInfo, gwApiId) { var options = { followAllRedirects: true, url: gwInfo.gwUrl+'/apis/'+gwApiId, agentOptions: {rejectUnauthorized: false}, headers: { 'Accept': 'application/json' } }; if (gwInfo.gwAuth) { options.headers.Authorization = 'Basic ' + gwInfo.gwAuth; } console.log('deleteApiFromGateway: request: '+JSON.stringify(options, " ", 2)); return new Promise(function(resolve, reject) { request.delete(options, function(error, response, body) { var statusCode = response ? response.statusCode : undefined; console.log('deleteApiFromGateway: response status:'+ statusCode); if (error) console.error('Warning: deleteGatewayApi request failed: '+ utils2.makeJsonString(error)); if (body) console.log('deleteApiFromGateway: response body: '+utils2.makeJsonString(body)); if (error) { console.error('deleteApiFromGateway: Unable to delete the API Gateway'); reject('Unable to delete the API Gateway: '+utils2.makeJsonString(error)); } else if (statusCode != 200) { if (body) { var errMsg = JSON.stringify(body); if (body.error && body.error.message) errMsg = body.error.message; reject('Unable to delete the API Gateway (status code '+statusCode+'): '+ errMsg); } else { reject('Unable to delete the API Gateway: Response failure code: '+statusCode); } } else { resolve(); } }); }); } /** * Return an array of APIs */ function getApis(gwInfo, tenantId, bpOrApiName) { var qsBasepath = { 'filter[where][basePath]' : bpOrApiName }; var qsApiName = { 'filter[where][name]' : bpOrApiName }; var qs; if (bpOrApiName) { if (bpOrApiName.indexOf('/') !== 0) { console.log('getApis: querying APIs based on api name'); qs = qsApiName; } else { console.log('getApis: querying APIs based on basepath'); qs = qsBasepath; } } var options = { followAllRedirects: true, url: gwInfo.gwUrl+'/tenants/'+tenantId+'/apis', headers: { 'Accept': 'application/json' }, }; if (qs) { options.qs = qs; } if (gwInfo.gwAuth) { options.headers.Authorization = 'Basic ' + gwInfo.gwAuth; } console.log('getApis: request: '+JSON.stringify(options)); return new Promise(function(resolve, reject) { request.get(options, function(error, response, body) { var statusCode = response ? response.statusCode : undefined; console.log('getApis: response status: '+ statusCode); if (error) console.error('Warning: getApis request failed: '+utils2.makeJsonString(error)); if (body) console.log('getApis: response body: '+utils2.makeJsonString(body)); if (error) { console.error('getApis: Unable to obtain API(s) from the API Gateway'); reject('Unable to obtain API(s) from the API Gateway: '+utils2.makeJsonString(error)); } else if (statusCode != 200) { if (body) { var errMsg = JSON.stringify(body); if (body.error && body.error.message) errMsg = body.error.message; reject('Unable to obtain API(s) from the API Gateway (status code '+statusCode+'): '+ errMsg); } else { reject('Unable to obtain API(s) from the API Gateway: Response failure code: '+statusCode); } } else { if (body) { try { var bodyJson = JSON.parse(body); if (Array.isArray(bodyJson)) { resolve(bodyJson); } else { console.error('getApis: Invalid API GW response body; a JSON array was not returned'); resolve( [] ); } } catch(e) { console.error('getApis: Invalid API GW response body; JSON.parse() failure: '+e); reject('Invalid API Gateway response: '+e); } } else { console.log('getApis: No APIs found'); resolve( [] ); } } }); }); } /** * Convert API object array into specified format * Parameters: * apis : array of 0 or more APIs * format : 'apigw' or 'swagger' * Returns: * array : New array of API object - each in the specified format */ function transformApis(apis, format) { var apisOutput; try { if (format.toLowerCase() === 'apigw') { apisOutput = apis; } else if (format.toLowerCase() === 'swagger') { apisOutput = JSON.parse(JSON.stringify(apis)); for (var i = 0; i < apisOutput.length; i++) { apisOutput[i] = generateSwaggerApiFromGwApi(apisOutput[i]); } } else { console.error('transformApis: Invalid format specification: '+format); throw 'Internal error. Invalid format specification: '+format; } } catch(e) { console.error('transformApis: exception caught: '+e); throw 'API format transformation error: '+e; } return apisOutput; } /** * Convert API object into swagger JSON format * Parameters: * gwApi : API object as returned from the API Gateway * Returns: * object : New API object in swagger JSON format */ function generateSwaggerApiFromGwApi(gwApi) { // Start with a copy of the gwApi object. It's close to the desired swagger format var swaggerApi = JSON.parse(JSON.stringify(gwApi)); swaggerApi.swagger = '2.0'; swaggerApi.info = { title: gwApi.name, version: '1.0.0' }; // Copy the gwAPI's 'resources' object as the starting point for the swagger 'paths' object swaggerApi.paths = JSON.parse(JSON.stringify(gwApi.resources)); for (var path in swaggerApi.paths) { if (!swaggerApi.paths[path]) { console.error('generateSwaggerApiFromGwApi: no operations defined for ignored relpath \''+path+'\''); delete swaggerApi.paths[path]; continue; } for (var op in swaggerApi.paths[path].operations) { console.log('generateSwaggerApiFromGwApi: processing path '+path+'; operation '+op); if (!op) { console.error('generateSwaggerApiFromGwApi: path \''+path+'\' has no operations!'); continue; } // swagger wants lower case operations var oplower = op.toLowerCase(); // Valid swagger requires a 'responses' object for each operation swaggerApi.paths[path][oplower] = { responses: { default: { description: 'Default response' } } }; // Custom swagger extension to hold the action mapping configuration swaggerApi.paths[path][oplower]['x-ibm-op-ext'] = { backendMethod : swaggerApi.paths[path].operations[op].backendMethod, backendUrl : swaggerApi.paths[path].operations[op].backendUrl, policies : JSON.parse(JSON.stringify(swaggerApi.paths[path].operations[op].policies)), actionName: getActionNameFromActionUrl(swaggerApi.paths[path].operations[op].backendUrl), actionNamespace: getActionNamespaceFromActionUrl(swaggerApi.paths[path].operations[op].backendUrl) }; } delete swaggerApi.paths[path].operations; } delete swaggerApi.resources; delete swaggerApi.name; delete swaggerApi.id; delete swaggerApi.managedUrl; delete swaggerApi.tenantId; return swaggerApi; } /** * Create a base swagger API object containing the API basepath, but no endpoints * Parameters: * basepath - Required. API basepath * apiname - Optional. API friendly name. Defaults to basepath * Returns: * swaggerApi - API swagger JSON object */ function generateBaseSwaggerApi(basepath, apiname) { var swaggerApi = { swagger: "2.0", info: { title: apiname || basepath, version: "1.0.0" }, basePath: basepath, paths: {} }; return swaggerApi; } /** * Take an API in JSON swagger format and create an API GW compatible * API configuration JSON object * Parameters: * swaggerApi - JSON object defining API in swagger format * Returns: * gwApi - JSON object defining API in API GW format */ function generateGwApiFromSwaggerApi(swaggerApi) { var gwApi = {}; gwApi.basePath = swaggerApi.basePath; gwApi.name = swaggerApi.info.title; gwApi.resources = {}; for (var path in swaggerApi.paths) { console.log('generateGwApiFromSwaggerApi: processing swaggerApi path: ', path); gwApi.resources[path] = {}; var gwpathop = gwApi.resources[path].operations = {}; for (var operation in swaggerApi.paths[path]) { console.log('generateGwApiFromSwaggerApi: processing swaggerApi operation: ', operation); console.log('generateGwApiFromSwaggerApi: processing operation backendMethod: ', swaggerApi.paths[path][operation]['x-ibm-op-ext'].backendMethod); var gwop = gwpathop[operation] = {}; gwop.backendMethod = swaggerApi.paths[path][operation]['x-ibm-op-ext'].backendMethod; gwop.backendUrl = swaggerApi.paths[path][operation]['x-ibm-op-ext'].backendUrl; gwop.policies = swaggerApi.paths[path][operation]['x-ibm-op-ext'].policies; } } return gwApi; } /** * Take an existing API in JSON swagger format, and update it with a single path/operation. * The addition can be an entirely new path or a new operation under an existing path. * Parameters: * swaggerApi - API to augment in swagger JSON format. This will be updated. * endpoint - JSON object describing new path/operation. Required fields * { * gatewayMethod: * gatewayPath: * action: { * authkey: * backendMethod: * backendUrl: * name: * namespace: * } * } * Returns: * swaggerApi - Input JSON object in swagger format containing the union of swaggerApi + new path/operation */ function addEndpointToSwaggerApi(swaggerApi, endpoint) { var operation = endpoint.gatewayMethod.toLowerCase(); var auth_base64 = Buffer.from(endpoint.action.authkey,'ascii').toString('base64'); // If the relative path already exists, append to it; otherwise create it if (!swaggerApi.paths[endpoint.gatewayPath]) { swaggerApi.paths[endpoint.gatewayPath] = {}; } swaggerApi.paths[endpoint.gatewayPath][operation] = { 'x-ibm-op-ext': { backendMethod: endpoint.action.backendMethod, backendUrl: endpoint.action.backendUrl, actionName: endpoint.action.name, actionNamespace: endpoint.action.namespace, policies: [ { type: 'reqMapping', value: [ { action: 'transform', from: { name: '*', location: 'query' }, to: { name: '*', location: 'body' } }, { action: 'insert', from: { value: 'Basic '+auth_base64 }, to: { name: 'Authorization', location: 'header' } }, { action: 'insert', from: { value: 'application/json' }, to: { name: 'Content-Type', location: 'header' } }, { action: 'insert', from: { value: 'true' }, to: { name: 'blocking', location: 'query' } }, { action: 'insert', from: { value: 'true' }, to: { name: 'result', location: 'query' } } ] } ] }, responses: { default: { description: "Default response" } } }; return swaggerApi; } /** * Update an existing DB API document by removing the specified relpath/operation section. * swaggerApi - API from which to remove the specified endpoint. This object will be updated. * endpoint - JSON object describing new path/operation. Required fields * { * gatewayPath: Optional. The relative path. If not provided, the original swaggerApi is returned * gatewayMethod: Optional. The operation under gatewayPath. If not provided, the entire gatewayPath is deleted. * If updated gatewayPath has no more operations, then the entire gatewayPath is deleted. * } * @returns Updated JSON swagger API */ function removeEndpointFromSwaggerApi(swaggerApi, endpoint) { var relpath = endpoint.gatewayPath; var operation = endpoint.gatewayMethod ? endpoint.gatewayMethod.toLowerCase() : endpoint.gatewayMethod; console.log('removeEndpointFromSwaggerApi: relpath '+relpath+' operation '+operation); if (!relpath) { console.log('removeEndpointFromSwaggerApi: No relpath specified; nothing to remove'); return 'No relpath provided; nothing to remove'; } // If an operation is not specified, delete the entire relpath if (!operation) { console.log('removeEndpointFromSwaggerApi: No operation; removing entire relpath '+relpath); if (swaggerApi.paths[relpath]) { delete swaggerApi.paths[relpath]; } else { console.log('removeEndpointFromSwaggerApi: relpath '+relpath+' does not exist in the API; already deleted'); return 'relpath '+relpath+' does not exist in the API'; } } else { if (swaggerApi.paths[relpath] && swaggerApi.paths[relpath][operation]) { delete swaggerApi.paths[relpath][operation]; if (Object.keys(swaggerApi.paths[relpath]).length === 0) { console.log('removeEndpointFromSwaggerApi: after deleting operation '+operation+', relpath '+relpath+' has no more operations; so deleting entire relpath '+relpath); delete swaggerApi.paths[relpath]; } } else { console.log('removeEndpointFromSwaggerApi: relpath '+relpath+' with operation '+operation+' does not exist in the API'); return 'relpath '+relpath+' with operation '+operation+' does not exist in the API'; } } return swaggerApi; } function confidentialPrint(str) { var printStr; if (str) { printStr = 'XXXXXXXXXX'; } return printStr; } /** * Create the CLI response payload from an array of GW API objects * Parameters: * gwApis - Array of JSON GW API objects * Returns: * respApis - A new array of JSON CLI API objects */ function generateCliResponse(gwApis) { var respApis = []; try { for (var i=0; i<gwApis.length; i++) { respApis.push(generateCliApiFromGwApi(gwApis[i])); } } catch(e) { console.error('generateCliResponse: exception caught: '+e); throw 'API format transformation error: '+e; } return respApis; } /** * Use the specified GW API object to create an API JSON object in for format the CLI expects. * Parameters: * gwApi - JSON GW API object * Returns: * cliApi - JSON CLI API object */ function generateCliApiFromGwApi(gwApi) { var cliApi = {}; cliApi.id = 'Not Used'; cliApi.key = 'Not Used'; cliApi.value = {}; cliApi.value.namespace = 'Not Used'; cliApi.value.gwApiActivated = true; cliApi.value.tenantId = 'Not Used'; cliApi.value.gwApiUrl = gwApi.managedUrl; cliApi.value.apidoc = generateSwaggerApiFromGwApi(gwApi); return cliApi; } /* * Parses the openwhisk action URL and returns the various components * Parameters * url - in format PROTOCOL://HOST/api/v1/namespaces/NAMESPACE/actions/ACTIONNAME * Returns * result - an array of strings. * result[0] : Entire URL * result[1] : protocol (i.e. https) * result[2] : host (i.e. myco.com, 1.2.3.4, myco.com/whisk) * result[3] : namespace * result[4] : action name, including the package if used (i.e. myaction, mypkg/myaction) */ function parseActionUrl(actionUrl) { var actionUrlPattern = /(\w+):\/\/([:\/\w.\-]+)\/api\/v\d\/namespaces\/([@\w .\-]+)\/actions\/([@\w .\-\/]+)/; try { return actionUrl.match(actionUrlPattern); } catch(e) { console.error('parseActionUrl: exception: '+e); throw 'parseActionUrl: exception: '+e; } } /* * https://172.17.0.1/api/v1/namespaces/whisk.system/actions/getaction * would return getaction * https://my-host.mycompany.com/api/v1/namespaces/myid@gmail.com_dev/actions/getaction * would return getaction * * https://172.17.0.1/api/v1/namespaces/whisk.system/actions/mypkg/getaction * would return mypkg/getaction * https://my-host.mycompany.com/api/v1/namespaces/myid@gmail.com_dev/actions/mypkg/getaction * would return mypkg/getaction */ function getActionNameFromActionUrl(actionUrl) { return parseActionUrl(actionUrl)[4]; } /* * https://172.17.0.1/api/v1/namespaces/whisk.system/actions/getaction * would return whisk.system * https://my-host.mycompany.com/api/v1/namespaces/myid@gmail.com_dev/actions/mypkg/getaction * would return myid@gmail.com_dev */ function getActionNamespaceFromActionUrl(actionUrl) { return parseActionUrl(actionUrl)[3]; } /* * Replace the namespace values that are used in the apidoc with the * specified namespace */ function updateNamespace(apidoc, namespace) { if (apidoc && namespace) { if (apidoc.action) { // The action namespace does not have to match the CLI user's namespace // If it is different, leave it alone; otherwise use the replacement namespace if (apidoc.namespace === apidoc.action.namespace) { apidoc.action.namespace = namespace; apidoc.action.backendUrl = replaceNamespaceInUrl(apidoc.action.backendUrl, namespace); } } apidoc.namespace = namespace; } } /* * Take an OpenWhisk URL (i.e. action invocation URL) and replace the namespace * path parameter value with the provided namespace value */ function replaceNamespaceInUrl(url, namespace) { var namespacesPattern = /\/namespaces\/([\w@.-]+)\//; console.log('replaceNamespaceInUrl: url before - '+url); matchResult = url.match(namespacesPattern); if (matchResult !== null) { console.log('replaceNamespaceInUrl: replacing namespace \''+matchResult[1]+'\' with \''+namespace+'\''); url = url.replace(namespacesPattern, '/namespaces/'+namespace+'/'); } console.log('replaceNamespaceInUrl: url after - '+url); return url; } module.exports.createTenant = createTenant; module.exports.getTenants = getTenants; module.exports.getApis = getApis; module.exports.addApiToGateway = addApiToGateway; module.exports.deleteApiFromGateway = deleteApiFromGateway; module.exports.generateBaseSwaggerApi = generateBaseSwaggerApi; module.exports.generateGwApiFromSwaggerApi = generateGwApiFromSwaggerApi; module.exports.transformApis = transformApis; module.exports.generateSwaggerApiFromGwApi = generateSwaggerApiFromGwApi; module.exports.addEndpointToSwaggerApi = addEndpointToSwaggerApi; module.exports.removeEndpointFromSwaggerApi = removeEndpointFromSwaggerApi; module.exports.confidentialPrint = confidentialPrint; module.exports.generateCliResponse = generateCliResponse; module.exports.generateCliApiFromGwApi = generateCliApiFromGwApi; module.exports.updateNamespace = updateNamespace;