blob: 36310fb1f14d841b30d3eacd60d3483889133620 [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.
*/
/*
* Add a new or update an existing API configuration in the API Gateway
* https://docs.cloudant.com/document.html#documentCreate
*
* Parameters (all as fields in the message JSON object)
* gwUrlV2 Required when accesstoken is provided. The V2 API Gateway base path (i.e. http://gw.com)
* gwUrl Required. The API Gateway base path (i.e. http://gw.com)
* gwUser Optional. The API Gateway authentication
* gwPwd Optional. The API Gateway authentication
* __ow_user Required. Namespace of API author. Set by controller
* The value overrides namespace values in the apidoc
* Don't override namespace values in the swagger though
* tenantInstance Optional. Instance identifier used when creating the specific API GW Tenant
* accesstoken Optional. Dynamic API GW auth. Overrides gwUser/gwPwd
* spaceguid Optional. Namespace unique id.
* responsetype Optional. web action response .extension to use. default to json
* apidoc Required. The API Gateway mapping document
* namespace Required. Namespace of user/caller
* apiName Optional if swagger not specified. API descriptive name
* gatewayBasePath Required if swagger not specified. API base path
* gatewayPath Required if swagger not specified. Specific API path (relative to base path)
* gatewayMethod Required if swagger not specified. API path operation
* id Optional if swagger not specified. Unique id of API
* action Required. if swagger not specified
* name Required. Action name (includes package)
* namespace Required. Action namespace
* backendMethod Required. Action invocation REST verb. "POST"
* backendUrl Required. Action invocation REST url
* authkey Required. Action invocation auth key
* secureKey Optional. Action's require-whisk-auth value
* swagger Required if gatewayBasePath not provided. API swagger JSON
*
* NOTE: The package containing this action will be bound to the following values:
* gwUrl, gwAuth
* As such, the caller to this action should normally avoid explicitly setting
* these values
*/
var utils = require('./utils.js');
var utils2 = require('./apigw-utils.js');
function main(message) {
//console.log('message: '+JSON.stringify(message)); // ONLY FOR TEMPORARY/LOCAL DEBUG; DON'T ENABLE PERMANENTLY
var badArgMsg = validateArgs(message);
if (badArgMsg) {
return Promise.reject(utils2.makeErrorResponseObject(badArgMsg, (message.__ow_method !== undefined)));
}
var gwInfo = {
gwUrl: message.gwUrl,
};
// Replace the CLI provided namespace values with the controller provided namespace value
// If __ow_user is not set, the namespace values are left alone
if (message.accesstoken) {
utils2.updateNamespace(message.apidoc, message.__ow_user);
} else {
utils.updateNamespace(message.apidoc, message.__ow_user);
}
// Set the User-Agent header value
if (message.__ow_headers && message.__ow_headers['user-agent']) {
utils2.setSubUserAgent(message.__ow_headers['user-agent']);
}
// message.apidoc already validated; creating shortcut to it
var doc;
if (typeof message.apidoc === 'object') {
doc = message.apidoc;
} else if (typeof message.apidoc === 'string') {
doc = JSON.parse(message.apidoc);
}
// message.swagger already validated; creating object
var swaggerObj;
if (typeof doc.swagger === 'object') {
swaggerObj = doc.swagger;
} else if (typeof doc.swagger === 'string') {
swaggerObj = JSON.parse(doc.swagger);
}
doc.swagger = swaggerObj;
var basepath = getBasePath(doc);
var tenantInstance = message.tenantInstance || 'openwhisk';
// This can be invoked as either a standard web action or as a normal action
var calledAsWebAction = message.__ow_method !== undefined;
// Log parameter values
console.log('GW URL : '+message.gwUrl);
console.log('GW URL V2 : '+message.gwUrlV2);
console.log('GW Auth : '+utils.confidentialPrint(message.gwPwd));
console.log('__ow_user : '+message.__ow_user);
console.log('namespace : '+doc.namespace);
console.log('tenantInstance: '+message.tenantInstance+' / '+tenantInstance);
console.log('accesstoken : '+message.accesstoken);
console.log('spaceguid : '+message.spaceguid);
console.log('responsetype : '+message.responsetype);
console.log('API name : '+doc.apiName);
console.log('basepath : '+basepath);
console.log('relpath : '+doc.gatewayPath);
console.log('GW method : '+doc.gatewayMethod);
if (doc.action) {
console.log('action name: '+doc.action.name);
console.log('action namespace: '+doc.action.namespace);
console.log('action backendUrl: '+doc.action.backendUrl);
console.log('action backendMethod: '+doc.action.backendMethod);
console.log('action authkey: '+utils.confidentialPrint(doc.action.authkey));
console.log('action secureKey: '+utils.confidentialPrint(doc.action.secureKey));
}
console.log('calledAsWebAction: '+calledAsWebAction);
console.log('apidoc :\n'+JSON.stringify(doc));
// If an API GW access token is provided, use the API GW V2 URL and use this token to auth with the API GW
// Otherwise, use the API GW "V1" URL and use the supplied GW auth credentials to auth with the API GW
if (message.accesstoken) {
var apiDocId;
gwInfo.gwUrl = message.gwUrlV2;
gwInfo.gwAuth = message.accesstoken;
// 1. If an existing API exists for this namespace/basepath combination, retrieve it and update it
// 2. If not, create a new API
return utils2.getApis(gwInfo, message.spaceguid, basepath)
.then(function(endpointDocs) {
console.log('Got '+endpointDocs.length+' APIs');
if (endpointDocs.length === 0) {
console.log('No API found for namespace '+doc.namespace + ' with basePath '+ basepath);
return Promise.resolve(utils2.generateBaseSwaggerApi(basepath, doc.apiName));
} else {
apiDocId = endpointDocs[0].artifact_id;
return Promise.resolve(endpointDocs[0].open_api_doc);
}
})
.then(function(endpointDoc) {
if (doc.swagger) {
console.log('Use provided swagger as the entire API; override any existing API');
return Promise.resolve(doc.swagger);
} else {
console.log('Add the provided API endpoint');
return Promise.resolve(utils2.addEndpointToSwaggerApi(endpointDoc, doc, message.responsetype));
}
})
.then(function(apiSwagger){
console.log("Validating Swagger doc before sending it to API GW.")
return utils2.validateFinalSwagger(apiSwagger);
})
.then(function(apiSwagger) {
console.log('Final swagger API config: '+ JSON.stringify(apiSwagger));
return utils2.addApiToGateway(gwInfo, message.spaceguid, apiSwagger, apiDocId);
})
.then(function(gwApi) {
console.log('API GW configured with API');
var cliApi = utils2.generateCliApiFromGwApi(gwApi).value;
console.log('createApi success');
return Promise.resolve(utils2.makeResponseObject(cliApi, calledAsWebAction));
})
.catch(function(reason) {
var rejmsg = 'API creation failure: ' + JSON.parse(utils2.makeJsonString(reason)); // Avoid unnecessary JSON escapes
console.error(rejmsg);
return Promise.reject(utils2.makeErrorResponseObject(rejmsg, calledAsWebAction));
});
} else {
// Create and activate a new API path
// 1. Create tenant id this namespace. If id exists, create is a noop
// 2. Obtain any existing configuration for the target API. If none, this is a new API
// 3. Create the API document to send to the API GW. If API exists, update it
// 4. Configure API GW with the new/updated API
var tenantId;
var gwApiId;
if (message.gwUser && message.gwPwd) {
gwInfo.gwAuth = Buffer.from(message.gwUser+':'+message.gwPwd,'ascii').toString('base64');
}
return utils.createTenant(gwInfo, doc.namespace, tenantInstance)
.then(function(tenant) {
console.log('Got the API GW tenant: '+JSON.stringify(tenant));
tenantId = tenant.id;
return Promise.resolve(utils.getApis(gwInfo, tenant.id, basepath));
})
.then(function(apis) {
console.log('Got '+apis.length+' APIs');
if (apis.length === 0) {
console.log('No APIs found for namespace '+doc.namespace+' with basepath '+basepath);
return Promise.resolve(utils.generateBaseSwaggerApi(basepath, doc.apiName));
} else if (apis.length > 1) {
console.error('Multiple APIs found for namespace '+doc.namespace+' with basepath '+basepath);
return Promise.reject('Internal error. Multiple APIs found for namespace '+doc.namespace+' with basepath '+basepath);
}
gwApiId = apis[0].id;
return Promise.resolve(utils.generateSwaggerApiFromGwApi(apis[0]));
})
.then(function(swaggerApi) {
if (doc.swagger) {
console.log('Use provided swagger as the entire API; override any existing API');
return Promise.resolve(doc.swagger);
} else {
console.log('Add the provided API endpoint');
return Promise.resolve(utils.addEndpointToSwaggerApi(swaggerApi, doc));
}
})
.then(function(apiSwagger){
console.log("Validating Swagger doc before sending it to API GW.")
return utils2.validateFinalSwagger(apiSwagger);
})
.then(function(swaggerApi) {
console.log('Final swagger API config: '+ JSON.stringify(swaggerApi));
return utils.addApiToGateway(gwInfo, tenantId, swaggerApi, gwApiId);
})
.then(function(gwApi) {
console.log('API GW configured with API');
var cliApi = utils.generateCliApiFromGwApi(gwApi).value;
console.log('createApi success');
return Promise.resolve(utils2.makeResponseObject(cliApi, calledAsWebAction));
})
.catch(function(reason) {
var rejmsg = 'API creation failure: ' + JSON.parse(utils2.makeJsonString(reason)); // Avoid unnecessary JSON escapes
console.error(rejmsg);
return Promise.reject(utils2.makeErrorResponseObject(rejmsg, calledAsWebAction));
});
}
}
function getBasePath(apidoc) {
if (apidoc.swagger) {
return apidoc.swagger.basePath;
}
return apidoc.gatewayBasePath;
}
function validateArgs(message) {
var tmpdoc;
if(!message) {
console.error('No message argument!');
return 'Internal error. A message parameter was not supplied.';
}
if (!message.gwUrl && !message.gwUrlV2) {
return 'gwUrl is required.';
}
if (!message.__ow_user) {
return 'A valid auth key is required.';
}
if(!message.apidoc) {
return 'apidoc is required.';
}
if (typeof message.apidoc == 'object') {
tmpdoc = message.apidoc;
} else if (typeof message.apidoc === 'string') {
try {
tmpdoc = JSON.parse(message.apidoc);
} catch (e) {
return 'apidoc field cannot be parsed. Ensure it is valid JSON.';
}
} else {
return 'apidoc field is of type ' + (typeof message.apidoc) + ' and should be a JSON object or a JSON string.';
}
if (!tmpdoc.namespace) {
return 'apidoc is missing the namespace field';
}
var tmpSwaggerDoc;
if(tmpdoc.swagger) {
if (tmpdoc.gatewayBasePath) {
return 'swagger and gatewayBasePath are mutually exclusive and cannot be specified together.';
}
if (typeof tmpdoc.swagger == 'object') {
tmpSwaggerDoc = tmpdoc.swagger;
} else if (typeof tmpdoc.swagger === 'string') {
try {
tmpSwaggerDoc = JSON.parse(tmpdoc.swagger);
} catch (e) {
return 'swagger field cannot be parsed. Ensure it is valid JSON.';
}
} else {
return 'swagger field is ' + (typeof tmpdoc.swagger) + ' and should be an object or a JSON string.';
}
console.log('Swagger JSON object: ', tmpSwaggerDoc);
if (!tmpSwaggerDoc.basePath) {
return 'swagger is missing the basePath field.';
}
if (!tmpSwaggerDoc.paths) {
return 'swagger is missing the paths field.';
}
if (!tmpSwaggerDoc.info) {
return 'swagger is missing the info field.';
}
} else {
if (!tmpdoc.gatewayBasePath) {
return 'apidoc is missing the gatewayBasePath field';
}
if (!tmpdoc.gatewayPath) {
return 'apidoc is missing the gatewayPath field';
}
if (!tmpdoc.gatewayMethod) {
return 'apidoc is missing the gatewayMethod field';
}
if (!tmpdoc.action) {
return 'apidoc is missing the action field.';
}
if (!tmpdoc.action.backendMethod) {
return 'action is missing the backendMethod field.';
}
if (!tmpdoc.action.backendUrl) {
return 'action is missing the backendUrl field.';
}
if (!tmpdoc.action.namespace) {
return 'action is missing the namespace field.';
}
if(!tmpdoc.action.name) {
return 'action is missing the name field.';
}
if (!tmpdoc.action.authkey) {
return 'action is missing the authkey field.';
}
}
return '';
}
module.exports.main = main;