blob: 7e99432c1b475ce680d4e91a930ae3714e5921b6 [file] [log] [blame]
/*
* 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 API GW utilities
*/
var request = require('request');
var _ = require('lodash');
const ApimgmtUserAgent = "OpenWhisk-apimgmt/1.0.0";
var UserAgent = ApimgmtUserAgent;
/**
* Helper method for the validateFinalSwagger function. Generates a map of operationId to target-url strings so we
* can validate that each operationId we find that has a parameter in the path also has its target-url appended with
* $(request.path)
*
* @param ibmConfig Required. The 'x-ibm-configuration' portion of the swaggerApi.
* @return A map of operationId->target-url pairs for checking.
*/
function generateTargetUrlMap(ibmConfig) {
var targetUrls = {};
ibmConfig['assembly']['execute'].forEach(function(exec) {
if (exec['operation-switch'] && exec['operation-switch']['case']) {
exec['operation-switch']['case'].forEach(function(element) {
var operations = element['operations'];
var execs = element['execute'];
//each nth element of execs and operations go together, so lets add those to the map.
for (var i = 0; i < operations.length ; ++i) {
if(i < execs.length && execs[i] && execs[i]['invoke'] && execs[i]['invoke']['target-url']) {
targetUrls[operations[i]] = execs[i]['invoke']['target-url'];
}
}
});
}
});
return targetUrls;
}
/**
* Helper function that just validates whether a relative path meets the following conditions:
* 1. It has not path parameters
* 2. If it has path parameters, that the parameters are well formed (i.e. each param is surrounded by {}).
*
* @param relativePath Required. The relative path we are checking.
* @return True if the path is valid, false otherwise.
*/
function isValidRelativePath(relativePath) {
var validParamRegex = /\/\{([^\/]+)\}\/|\/\{([^\/]+)\}$/g;
if (relativePath.match(validParamRegex)) {
return true;
}
return false;
}
/**
* Simple function to get the name of each path parameter defined in the path.
*
* @param path Required. The path we are checking.
* @return An array that contains each named path parameter, or an empty list if none are found.
*/
function getPathParameters(relativePath) {
var params = [];
var validNameRegex = /\{([^\/]+)\}/g
//Match returns all the matches found, including the {} chars. so we have to remove them.
var namesFound = relativePath.match(validNameRegex);
if (namesFound) {
params = namesFound.map(function (pathName){
return pathName.substring(1,pathName.length-1);
});
}
return params;
}
/**
* Currently this only checks the final swagger that will be passed into API GW whether the path parameter definition
* is correct.
*
* @param swaggerApi Required. The API swagger object to send to the API gateway
* @return A promise with the fully validated swaggerApi, or an error response if rejected.
*/
function validateFinalSwagger(swaggerApi) {
return new Promise(function(resolve, reject) {
// This returns a map of urls to check for path parameters.
console.log("validateFinalSwagger: Validating swapper before posting to API GW.")
var errorMsg;
var paths = swaggerApi['paths'];
if (swaggerApi.basePath && isValidRelativePath(swaggerApi.basePath)) {
errorMsg = "The base path (" + swaggerApi.basePath + ") cannot have parameters. Only the relative path supports path parameters.";
}
/*
* This code will look at each path defined, and look at all the parameters in each path, and validate that each
* verb (GET,POST, etc) for each path defines parameter objects for each parameter defined in the path. For each of
* these that contain parameters, it will also check that its target-url ends in $(request.path).
* #beginPathValidation
*/
var targetUrlMap = generateTargetUrlMap(swaggerApi['x-ibm-configuration']);
for (var key in paths) {
if (errorMsg) { break; }
var idx = 0;
if (isValidRelativePath(key)) {
//Path is valid, lets check that we have parameters defined for each path parameter and that the target-url
//has $(request.path) at the end.
var namedParamsInPath = getPathParameters(key);
//Loop over each verb (GET,POST,etc), each should contain path parameters.
var parameters = paths[key]['parameters'] ? paths[key]['parameters'] : [];
for (var httpType in paths[key]) {
if (httpType == "parameters") {
continue;
}
var xOpenWhisk = paths[key][httpType]['x-openwhisk']
if (xOpenWhisk && xOpenWhisk['url'] && !xOpenWhisk['url'].endsWith('.http')) {
errorMsg = "The action must use a response type of '.http' in order to receive the path parameters.";
break;
}
var opId = paths[key][httpType].operationId;
if (targetUrlMap[opId] && !targetUrlMap[opId].endsWith('.http$(request.path)')) {
errorMsg = "The target-url for operationId '" + opId;
errorMsg += "' must end in '$(request.path)' in order for actions to receive the path parameters.";
break;
}
var allParams = parameters.concat(paths[key][httpType].parameters);
for (var i = 0 ; i < namedParamsInPath.length ; ++i) {
var found = false;
for (var j in allParams) {
if (allParams[j].name == namedParamsInPath[i]) {
found = true;
break;
}
}
if (!found) {
errorMsg = "The parameter '" + namedParamsInPath[i] + "' defined in path '" + key + "' does not match any";
errorMsg += " of the parameters defined for the path in the swagger file.";
break;
}
}
if(errorMsg) { break; }
}
if(errorMsg) { break; }
}
}
//#endPathValidation
if (errorMsg) {
console.error("validateFinalSwagger:" + errorMsg)
reject(errorMsg);
} else {
console.log("validateFinalSwagger: Validation of swagger before posting to API GW was successful.")
resolve(swaggerApi);
}
});
}
/**
* 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 user bearer token used to access the API Gateway REST endpoints
* @param spaceGuid Required. User's space guid. APIs are stored under this context
* @param swaggerApi Required. The API swagger object to send to the API gateway
* @param apiId Required. API id. When specified, the API exists and will be updated; otherwise the API is created anew
* @return A promise for an object describing the result with fields error and response
*/
function addApiToGateway(gwInfo, spaceGuid, swaggerApi, apiId) {
var requestFcn = request.post;
console.log('addApiToGateway: ');
try {
var options = {
followAllRedirects: true,
url: gwInfo.gwUrl+'/'+encodeURIComponent(spaceGuid) + '/apis',
json: swaggerApi, // Use of json automatically sets header: 'Content-Type': 'application/json'
headers: {
'User-Agent': UserAgent
}
};
if (gwInfo.gwAuth) {
_.set(options, "headers.Authorization", 'Bearer ' + gwInfo.gwAuth);
}
if (apiId) {
console.log("addApiToGateway: Updating existing API");
options.url = gwInfo.gwUrl + '/' + encodeURIComponent(spaceGuid) + '/apis/' + encodeURIComponent(apiId);
requestFcn = request.put;
}
console.log('addApiToGateway: request: '+JSON.stringify(options, " ", 2));
}
catch (e) {
console.error('addApiToGateway exception: '+e);
}
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: '+ makeJsonString(error));
if (response && response.headers) console.log('addApiToGateway: response headers: '+makeJsonString(response.headers));
if (body) console.log('addApiToGateway: response body: '+makeJsonString(body));
if (error) {
console.error('addApiToGateway: Unable to configure the API Gateway');
reject('Unable to configure the API Gateway: '+makeJsonString(error));
} else if (statusCode != 200) {
if (body) {
var errMsg = makeJsonString(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 spaceGuid Required. User's space guid. APIs are stored under this context
* @param apiId Required. API basepath. Unique per spaceGuid
* @return A promise for an object describing the result with fields error and response
*/
function deleteApiFromGateway(gwInfo, spaceGuid, apiId) {
var options = {
followAllRedirects: true,
url: gwInfo.gwUrl+'/'+encodeURIComponent(spaceGuid)+'/apis/'+encodeURIComponent(apiId),
agentOptions: {rejectUnauthorized: false},
headers: {
'Accept': 'application/json',
'User-Agent': UserAgent
}
};
if (gwInfo.gwAuth) {
options.headers.Authorization = 'Bearer ' + gwInfo.gwAuth;
}
console.log('deleteApiFromGateway: request: '+JSON.stringify(options));
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: '+ makeJsonString(error));
if (body) console.log('deleteApiFromGateway: response body: '+makeJsonString(body));
if (response && response.headers) console.log('deleteApiFromGateway: response headers: '+makeJsonString(response.headers));
if (error) {
console.error('deleteApiFromGateway: Unable to delete the API Gateway');
reject('Unable to delete the API Gateway: '+makeJsonString(error));
} else if (statusCode != 200 && statusCode != 204) {
if (body) {
var errMsg = makeJsonString(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, spaceGuid, bpOrApiName, limit, skip) {
var qsBasepath = { 'basePath' : bpOrApiName };
var qsApiName = { 'title' : 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+'/'+encodeURIComponent(spaceGuid)+'/apis?limit='+limit+'&skip='+skip,
headers: {
'Accept': 'application/json',
'User-Agent': UserAgent
},
json: true
};
if (qs) {
options.qs = qs;
}
if (gwInfo.gwAuth) {
options.headers.Authorization = 'Bearer ' + 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: '+makeJsonString(error));
if (response && response.headers) console.log('getApis: response headers: '+makeJsonString(response.headers));
console.log('getApis: body type = '+typeof body);
if (body) console.log('getApis: response JSON.stringify(body): '+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: '+makeJsonString(error));
} else if (statusCode != 200) {
console.error('getApis: failure: response code: '+statusCode);
if (body) {
var errMsg = makeJsonString(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) {
if (Array.isArray(body)) {
resolve(body);
} else {
console.error('getApis: Invalid API GW response body; a JSON array was not returned');
resolve( [] );
}
} 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;
}
/*
* 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;
}
/*
* 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': {},
'x-ibm-configuration': {
'assembly': {
},
'cors': {
'enabled': true
}
}
};
return swaggerApi;
}
/*
* 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:
* secureKey
* }
* }
* responsetype Optional. The web action invocation .extension. Defaults to json
* Returns:
* swaggerApi - Input JSON object in swagger format containing the union of swaggerApi + new path/operation
*/
function addEndpointToSwaggerApi(swaggerApi, endpoint, responsetype) {
var operation = endpoint.gatewayMethod.toLowerCase();
var operationId = makeOperationId(operation, endpoint.gatewayPath);
responsetype = responsetype || 'json';
console.log('addEndpointToSwaggerApi: operationid = '+operationId);
try {
// 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] = {
'operationId': operationId,
'parameters': endpoint.pathParameters,
'x-openwhisk': {
'url': makeWebActionBackendUrl(endpoint.action, responsetype),
'namespace': endpoint.action.namespace,
'package': getPackageNameFromFqActionName(endpoint.action.name),
'action': getActionNameFromFqActionName(endpoint.action.name),
},
'responses': {
'default': {
'description': 'Default response'
}
}
};
// API GW extensions
console.log('addEndpointToSwaggerApi: setting api gw extension values');
setActionOperationInvocationDetails(swaggerApi, endpoint, operationId, responsetype);
}
catch(e) {
console.log("addEndpointToSwaggerApi: exception "+e);
throw 'API swagger generation error: '+e;
}
return swaggerApi;
}
function setActionOperationInvocationDetails(swagger, endpoint, operationId, responsetype) {
var caseArr = _.get(swagger, 'x-ibm-configuration.assembly.execute[0].operation-switch.case') || [];
var caseIdx = getCaseOperationIdx(caseArr, operationId);
var operations = [operationId];
_.set(swagger, 'x-ibm-configuration.assembly.execute[0].operation-switch.case['+caseIdx+'].operations', operations);
_.set(swagger, 'x-ibm-configuration.assembly.execute[0].operation-switch.case['+caseIdx+'].execute[0].invoke.target-url', makeWebActionBackendUrl(endpoint.action, responsetype, true) );
_.set(swagger, 'x-ibm-configuration.assembly.execute[0].operation-switch.case['+caseIdx+'].execute[0].invoke.verb', 'keep');
if (endpoint.action.secureKey) {
_.set(swagger, 'x-ibm-configuration.assembly.execute[0].operation-switch.case['+caseIdx+'].execute[1].set-variable.actions[0].set', 'message.headers.X-Require-Whisk-Auth' );
_.set(swagger, 'x-ibm-configuration.assembly.execute[0].operation-switch.case['+caseIdx+'].execute[1].set-variable.actions[0].value', endpoint.action.secureKey );
}
}
// Return the numeric index into case[] into which the associated operation will be configured
// If the array is empty, the returned index is 0
// If the operation exists, the existing index will be returned
// Otherwise the index will be the last existing index + 1
function getCaseOperationIdx(caseArr, operationId) {
var i;
for (i=0; i<caseArr.length; i++) {
if (caseArr[i].operations[0] == operationId) {
console.log('getCaseOperationIdx: found existing operation for '+operationId+' at case index '+i);
break;
}
}
return i;
}
// Create the external URL used to invoke a web-action. Examples:
// - https://localhost/api/v1/web/whisk.system/default/echo-web.json
// - https://localhost/api/v1/web/whisk.system/mypkg/echo-web.json
// NOTE: Use "default" as the package name when a package is not explicitly defined.
// Parameters
// endpointAction - fully qualified action name (i.e. /ns/pkg/action or /ns/action)
// endpointResponseType - determines the action invocation extension without the '.' (i.e. http, json, etc)
// parameters - the parameters defined in the path, if any.
// Returns:
// string - web-action URL
function makeWebActionBackendUrl(endpointAction, endpointResponseType, isTargetUrl = false) {
protocol = getProtocolFromActionUrl(endpointAction.backendUrl);
host = getHostFromActionUrl(endpointAction.backendUrl);
ns = endpointAction.namespace;
pkg = getPackageNameFromFqActionName(endpointAction.name) || 'default';
name = getActionNameFromFqActionName(endpointAction.name);
reqPath = isTargetUrl && endpointResponseType === 'http' ? "$(request.path)" : "";
return protocol + '://' + host + '/api/v1/web/' + ns + '/' + pkg + '/' + name + '.' + endpointResponseType + reqPath;
}
/*
* Update an existing Swagger 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 path 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]) {
for (var op in swaggerApi.paths[relpath]) {
deleteActionOperationInvocationDetails(swaggerApi, makeOperationId(op, relpath));
}
delete swaggerApi.paths[relpath];
} else {
console.log('removeEndpointFromSwaggerApi: relpath '+relpath+' does not exist in the API');
return 'path \''+relpath+'\' does not exist in the API';
}
} else { // relpath and operation are specified, just delete the specific operation
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];
}
deleteActionOperationInvocationDetails(swaggerApi, makeOperationId(operation, relpath));
} else {
console.log('removeEndpointFromSwaggerApi: relpath '+relpath+' with operation '+operation+' does not exist in the API');
return 'path \''+relpath+'\' with operation \''+operation+'\' does not exist in the API';
}
}
return swaggerApi;
}
function deleteActionOperationInvocationDetails(swagger, operationId) {
console.log('deleteActionOperationInvocationDetails: deleting case entry for ' + operationId);
var caseArr = _.get(swagger, 'x-ibm-configuration.assembly.execute[0].operation-switch.case') || [];
if (caseArr.length > 0) {
var caseIdx = getCaseOperationIdx(caseArr, operationId);
_.pullAt(caseArr, caseIdx);
_.set(swagger, 'x-ibm-configuration.assembly.execute[0].operation-switch.case', caseArr);
} else {
console.log('deleteActionOperationInvocationDetails: empty case[] array; case operation '+operationId+' does not exist');
}
}
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) {
console.log('generateCliApiFromGwApi: ' + JSON.stringify(gwApi, " ", 2));
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.managed_url;
cliApi.value.apidoc = gwApi.open_api_doc;
return cliApi;
}
/*
* Parses the openwhisk action URL and returns the various components
* Parameters
* url - in format PROTOCOL://HOST/api/v1/web/NAMESPACE/PACKAGE/ACTION.http
* 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/mywhisk)
* result[3] : namespace
* result[4] : package name
* result[5] : action name
* result[6] : action response type (i.e http, json, text, html, or svg)
*/
function parseActionUrl(actionUrl) {
console.log('parseActionUrl: parsing action url: '+actionUrl);
var actionUrlPattern = /(\w+):\/\/([:\/\w.\-]+)\/api\/v\d\/web\/([@\w .\-]+)\/([@\w .\-]+)\/([@\w .\-\/]+)\.(\w+)/;
try {
return actionUrl.match(actionUrlPattern);
} catch(e) {
console.error('parseActionUrl: exception: '+e);
throw 'parseActionUrl: exception: '+e;
}
}
/*
* https://172.17.0.1/api/v1/web/NAMESPACE/PACKAGE/ACTION.json
* would return ACTION
*/
function getActionNameFromActionUrl(actionUrl) {
return parseActionUrl(actionUrl)[5];
}
/*
* https://172.17.0.1/api/v1/web/NAMESPACE/PACKAGE/ACTION.json
* would return NAMESPACE
*/
function getPackageNameFromActionUrl(actionUrl) {
return parseActionUrl(actionUrl)[4];
}
/*
* https://172.17.0.1/api/v1/web/NAMESPACE/PACKAGE/ACTION.json
* would return NAMESPACE
*/
function getActionNamespaceFromActionUrl(actionUrl) {
return parseActionUrl(actionUrl)[3];
}
/*
* https://172.17.0.1/api/v1/namespaces/whisk.system/actions/getaction
* would return 172.17.0.1
* https://my-host.mycompany.com/api/v1/namespaces/myid@gmail.com_dev/actions/mypkg/getaction
* would return my-host.mycompany.com
*/
function getHostFromActionUrl(actionUrl) {
return parseActionUrl(actionUrl)[2];
}
/*
* https://172.17.0.1/api/v1/namespaces/whisk.system/actions/getaction
* would return https
*/
function getProtocolFromActionUrl(actionUrl) {
return parseActionUrl(actionUrl)[1];
}
/*
* Parses an openwhisk action name into its various components
* Parameters
* fqname - in one of the following formats:
* (1) /[namespace]/[package]/[action]
* (2) [package]/[action]
* (3) [action]
* Returns
* result - an array of strings; depending on input
* Input (1):
* result[0] : fqname (i.e. /ns/pkg/action)
* result[1] : namespace
* result[2] : package
* result[3] : action name
* Input (2):
* result[0] : fqname (i.e. pkg/action)
* result[1] : package
* result[2] : action name
* result[3] : ''
* Input (3):
* result[0] : fqname (i.e. action)
* result[1] : action name
* result[2] : ''
* result[3] : ''
*/
function parseActionName(fqname) {
console.log('parseActionName: parsing action: '+fqname);
var actionNamePattern = /[\/]?([@ .\-\w]*)[\/]?([@ .\-\w]*)[\/]?([@ .\-\w]*)/;
try {
return fqname.match(actionNamePattern);
} catch(e) {
console.error('parseActionName: exception: '+e);
throw 'parseActionName: exception: '+e;
}
}
function getNamespaceFromFqActionName(fqAction) {
var ns = '';
var parsedAction = parseActionName(fqAction);
if (parsedAction[3].length > 0) {
ns = parsedAction[1];
}
return ns;
}
function getPackageNameFromFqActionName(fqAction) {
var pkg = '';
var parsedAction = parseActionName(fqAction);
if (parsedAction[3].length > 0) {
pkg = parsedAction[2];
} else if (parsedAction[2].length > 0) {
pkg = parsedAction[1];
}
return pkg;
}
function getActionNameFromFqActionName(fqAction) {
var action = '';
var parsedAction = parseActionName(fqAction);
if (parsedAction[3].length > 0) {
action = parsedAction[3];
} else if (parsedAction[2].length > 0) {
action = parsedAction[2];
} else {
action = parsedAction[1];
}
return action;
}
/*
* 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
// And only replace when the namespace is the default '_' which needs replacement
if (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 = /\/api\/v1\/web\/([\w@.-]+)\//;
console.log('replaceNamespaceInUrl: namspace='+namespace+' url before - '+url);
matchResult = url.match(namespacesPattern);
if (matchResult !== null) {
console.log('replaceNamespaceInUrl: replacing namespace \''+matchResult[1]+'\' with \''+namespace+'\'');
url = url.replace(namespacesPattern, '/api/v1/web/'+namespace+'/');
}
console.log('replaceNamespaceInUrl: url after - '+url);
return url;
}
/*
* Take an error string and create a response object suitable for inclusion in
* a Promise.reject() call.
*
* The response object can take two formats. If the api management action was
* invoked as a web-action (i.e. via https://OW-HOST/api/v1/web/NS/PKG/ACTION.http),
* then the response is an error object that mimics a non-webaction openwhisk
* action's application error response - like so:
* {
* statusCode: 502, <- signifies an application error
* headers: {'Content-Type': 'application/json'},
* body: JSON object or JSON string
* }
* Otherwise, the action was invoked as a regular OpenWhisk action
* (i.e. https://OW-HOST/api/v1/namesapces/NS/actions/ACTION) and the
* error response is just a string. OpenWhisk backend logic will ultimately
* convert this string into the above error object format.
*
* Parameters
* err - Error string
* isWebAction - Boolean. True -> generate a web-action response
* False -> Generate an action response
*/
function makeErrorResponseObject(err, isWebAction) {
console.log('makeErrorResponseObject: isWebAction: '+isWebAction);
if (!isWebAction) {
console.log('makeErrorResponseObject: not called as a web action');
return err;
}
var bodystr = err;
if (typeof err === 'string') {
bodystr = {
"error": JSON.parse(makeJsonString(err)), // Make sure err is plain old string to avoid duplicate JSON escaping
};
}
return {
statusCode: 502,
headers: { 'Content-Type': 'application/json' },
body: bodystr
};
}
/*
* Take an response string and create a response object suitable for inclusion in
* a Promise.resolve() call.
*
* The response object can take two formats. If the api management action was
* invoked as a web-action (i.e. via https://OW-HOST/api/v1/web/NS/PKG/ACTION.http),
* then the response is an object that mimics a non-webaction openwhisk
* action's application successful response - like so:
* {
* statusCode: 200, <- signifies a successful action
* headers: {'Content-Type': 'application/json'},
* body: JSON object or JSON string
* }
* Otherwise, the action was invoked as a regular OpenWhisk action
* (i.e. https://OW-HOST/api/v1/namesapces/NS/actions/ACTION) and the
* response is just a string. OpenWhisk backend logic will ultimately
* convert this string into the above object format.
*
* Parameters
* err - Error string
* isWebAction - Boolean. True -> generate a web-action response
* False -> generate an action response
*/
function makeResponseObject(resp, isWebAction) {
console.log('makeResponseObject: isWebAction: '+isWebAction);
if (!isWebAction) {
console.log('makeResponseObject: not called as a web action');
return resp;
}
var bodystr = resp;
if (typeof resp === 'string') {
bodystr = JSON.parse(makeJsonString(resp));
}
retobj = {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: bodystr
};
return retobj;
}
/*
* Take an object and serialize it into a JSON string.
*
* Special consideration is give to strings that are already JSON formatted since
* serializing these strings can result in redundant escaping.
*
* If the value is simply not JSON compliant, a JSON error string is returned.
*/
function makeJsonString(x) {
// If the value is not already a string, rely on JSON.stringify to convert it correctly
if (x instanceof Error) {
//Print the whole error here as we are only returning the error message and nothing else.
console.error(x);
return JSON.stringify(x.message);
} else if (typeof x != 'string') {
try {
return JSON.stringify(x);
} catch (e) {
console.error('makeJsonString: value cannot be JSON serialized: '+e);
return e;
}
} else {
// It's a string. If it's already a JSON formatted string, leave it alone
// Otherwise, convert it into a JSON formatted string
try {
var temp = JSON.parse(x);
return x;
} catch (e) {
// The string is not a JSON string, so convert it to a JSON string.
console.log('makeJsonString: String is not JSON, so need to convert it: '+e);
return JSON.stringify(x);
}
}
return 'Unexpected JSON parsing failure';
}
/*
* Generate and return a swagger OperationId value
*
* Parameters
* operation - String. HTTP method (i.e. get, post, etc)
* repath - String. Swagger path value. The path relative to the base path
*/
function makeOperationId(operation, relpath) {
// Concatenate operation + relpath, stripping '/' and camelCasing after each '/' delimiter
// relpath special character handling in each path segment:
// . ~ ! $ & ' ( ) * + , ; = : @ are removed and the following characters in the same path segment are camel cased
// - _ are retained and the following characters in the same path segment are lower cased
return operation.toLowerCase() +
relpath.replace(/[^0-9a-z_-]/gi, ' ').replace(/\w\S*/g, function(word) {return makeCamelCase(word);}).replace(/\s/g, '');
}
function makeCamelCase(str) {
return str.charAt(0).toUpperCase() + str.substr(1).toLowerCase();
}
function setSubUserAgent(subAgent) {
if (subAgent && subAgent.length > 0) {
UserAgent = UserAgent + " " + subAgent;
}
}
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;
module.exports.makeErrorResponseObject = makeErrorResponseObject;
module.exports.makeResponseObject = makeResponseObject;
module.exports.makeJsonString = makeJsonString;
module.exports.setSubUserAgent = setSubUserAgent;
module.exports.validateFinalSwagger = validateFinalSwagger;