Add the ability to specify api via swagger in manifest (#1078)

* Add the ability to specify api via swagger in manifest

  Adds the ability for swagger file specifying the api to be
  referenced in the manifest under project.config. The config
  name was chosen to be consistent with whisk-cli -c/--config
  option.

* Fix bug in undeploy with no swagger and update godeps

  * Fix nil pointer error when no swagger present in undeploy
  * update godeps

* Add documentation and examples for new open api spec support

* Fix Godeps

* Remove trailing whitespace from open api spec doc

* Reword documentation

* Add error check

* Give a more descriptive title for documentation
diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json
index 1b463cb..28eb422 100644
--- a/Godeps/Godeps.json
+++ b/Godeps/Godeps.json
@@ -164,9 +164,13 @@
 			"ImportPath": "gopkg.in/yaml.v2",
 			"Rev": "eb3733d160e74a9c7e442f435eb3bea458e1d19f"
 		},
-        {
-            "ImportPath": "github.com/palantir/stacktrace",
-            "Rev": "78658fd2d1772b755720ed8c44367d11ee5380d6"
-        }
+    {
+      "ImportPath": "github.com/palantir/stacktrace",
+      "Rev": "78658fd2d1772b755720ed8c44367d11ee5380d6"
+    },
+    {
+      "ImportPath": "github.com/ghodss/yaml",
+      "Rev": "25d852aebe32c875e9c044af3eef9c7dc6bc777f"
+    }
 	]
 }
diff --git a/deployers/manifestreader.go b/deployers/manifestreader.go
index 4fa3308..8ef9638 100644
--- a/deployers/manifestreader.go
+++ b/deployers/manifestreader.go
@@ -113,6 +113,11 @@
 		return wskderrors.NewYAMLFileFormatError(manifestName, err)
 	}
 
+	api, response, err := manifestParser.ComposeApiRecordsFromSwagger(reader.serviceDeployer.ClientConfig, manifest)
+	if err != nil {
+		return wskderrors.NewYAMLFileFormatError(manifestName, err)
+	}
+
 	err = reader.SetDependencies(deps)
 	if err != nil {
 		return wskderrors.NewYAMLFileFormatError(manifestName, err)
@@ -143,6 +148,11 @@
 		return wskderrors.NewYAMLFileFormatError(manifestName, err)
 	}
 
+	err = reader.SetSwaggerApi(api, response)
+	if err != nil {
+		return wskderrors.NewYAMLFileFormatError(manifestName, err)
+	}
+
 	return nil
 }
 
@@ -322,6 +332,16 @@
 	return nil
 }
 
+func (reader *ManifestReader) SetSwaggerApi(api *whisk.ApiCreateRequest, response *whisk.ApiCreateRequestOptions) error {
+	dep := reader.serviceDeployer
+
+	dep.mt.Lock()
+	defer dep.mt.Unlock()
+	dep.Deployment.SwaggerApi = api
+	dep.Deployment.SwaggerApiOptions = response
+	return nil
+}
+
 // Check action record before deploying it
 // action record is created by reading and composing action elements from manifest file
 // Action.kind is mandatory which is set to
diff --git a/deployers/servicedeployer.go b/deployers/servicedeployer.go
index 770ee6c..e59b47d 100644
--- a/deployers/servicedeployer.go
+++ b/deployers/servicedeployer.go
@@ -45,11 +45,13 @@
 )
 
 type DeploymentProject struct {
-	Packages   map[string]*DeploymentPackage
-	Triggers   map[string]*whisk.Trigger
-	Rules      map[string]*whisk.Rule
-	Apis       map[string]*whisk.ApiCreateRequest
-	ApiOptions map[string]*whisk.ApiCreateRequestOptions
+	Packages          map[string]*DeploymentPackage
+	Triggers          map[string]*whisk.Trigger
+	Rules             map[string]*whisk.Rule
+	Apis              map[string]*whisk.ApiCreateRequest
+	ApiOptions        map[string]*whisk.ApiCreateRequestOptions
+	SwaggerApi        *whisk.ApiCreateRequest
+	SwaggerApiOptions *whisk.ApiCreateRequestOptions
 }
 
 func NewDeploymentProject() *DeploymentProject {
@@ -808,11 +810,21 @@
 
 // Deploy Apis into OpenWhisk
 func (deployer *ServiceDeployer) DeployApis() error {
-	for _, api := range deployer.Deployment.Apis {
-		err := deployer.createApi(api)
+	var err error
+	// NOTE: Only deploy either swagger or manifest defined api, but not both
+	// NOTE: Swagger API takes precedence
+	if deployer.Deployment.SwaggerApi != nil && deployer.Deployment.SwaggerApiOptions != nil {
+		err = deployer.createSwaggerApi(deployer.Deployment.SwaggerApi)
 		if err != nil {
 			return err
 		}
+	} else {
+		for _, api := range deployer.Deployment.Apis {
+			err = deployer.createApi(api)
+			if err != nil {
+				return err
+			}
+		}
 	}
 	return nil
 }
@@ -1035,6 +1047,27 @@
 	return nil
 }
 
+// create api (API Gateway functionality) from swagger file
+func (deployer *ServiceDeployer) createSwaggerApi(api *whisk.ApiCreateRequest) error {
+	var err error
+	var response *http.Response
+
+	apiCreateReqOptions := deployer.Deployment.SwaggerApiOptions
+	apiCreateReqOptions.SpaceGuid = strings.Split(deployer.Client.Config.AuthToken, ":")[0]
+	apiCreateReqOptions.AccessToken = deployer.Client.Config.ApigwAccessToken
+
+	err = retry(DEFAULT_ATTEMPTS, DEFAULT_INTERVAL, func() error {
+		_, response, err = deployer.Client.Apis.Insert(api, apiCreateReqOptions, true)
+		return err
+	})
+
+	if err != nil {
+		return createWhiskClientError(err.(*whisk.WskError), response, parsers.YAML_KEY_API, true)
+	}
+
+	return nil
+}
+
 func (deployer *ServiceDeployer) UnDeploy(verifiedPlan *DeploymentProject) error {
 	if deployer.Preview == true {
 		deployer.printDeploymentAssets(verifiedPlan)
@@ -1058,6 +1091,9 @@
 }
 
 func (deployer *ServiceDeployer) unDeployAssets(verifiedPlan *DeploymentProject) error {
+	if err := deployer.UndeploySwaggerApis(verifiedPlan); err != nil {
+		return err
+	}
 	if err := deployer.UnDeployApis(verifiedPlan); err != nil {
 		return err
 	}
@@ -1239,6 +1275,15 @@
 	return nil
 }
 
+func (deployer *ServiceDeployer) UndeploySwaggerApis(deployment *DeploymentProject) error {
+	api := deployment.SwaggerApi
+	err := deployer.deleteSwaggerApi(api)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
 func (deployer *ServiceDeployer) deletePackage(packa *whisk.Package) error {
 
 	displayPreprocessingInfo(parsers.YAML_KEY_PACKAGE, packa.Name, false)
@@ -1411,6 +1456,42 @@
 	return nil
 }
 
+// delete api (API Gateway functionality) from swagger file
+func (deployer *ServiceDeployer) deleteSwaggerApi(api *whisk.ApiCreateRequest) error {
+	var err error
+	var response *http.Response
+
+	// If there is no swagger file do nothing
+	if api == nil {
+		return nil
+	}
+	swaggerString := api.ApiDoc.Swagger
+	swaggerObj := new(whisk.ApiSwagger)
+	err = json.Unmarshal([]byte(swaggerString), swaggerObj)
+	if err != nil {
+		return err
+	}
+
+	apiDeleteReqOptions := new(whisk.ApiDeleteRequestOptions)
+	apiDeleteReqOptions.SpaceGuid = strings.Split(deployer.Client.Config.AuthToken, ":")[0]
+	apiDeleteReqOptions.AccessToken = deployer.Client.Config.ApigwAccessToken
+	apiDeleteReqOptions.ApiBasePath = swaggerObj.BasePath
+
+	a := new(whisk.ApiDeleteRequest)
+	a.Swagger = swaggerString
+
+	err = retry(DEFAULT_ATTEMPTS, DEFAULT_INTERVAL, func() error {
+		response, err = deployer.Client.Apis.Delete(a, apiDeleteReqOptions)
+		return err
+	})
+
+	if err != nil {
+		return createWhiskClientError(err.(*whisk.WskError), response, parsers.YAML_KEY_API, true)
+	}
+
+	return nil
+}
+
 // Utility function to call go-whisk framework to delete action
 func (deployer *ServiceDeployer) deleteAction(pkgname string, action *whisk.Action) error {
 	// call ActionService through Client
diff --git a/docs/examples/manifest_hello_world_apigateway_open_api_spec.yaml b/docs/examples/manifest_hello_world_apigateway_open_api_spec.yaml
new file mode 100644
index 0000000..26826e9
--- /dev/null
+++ b/docs/examples/manifest_hello_world_apigateway_open_api_spec.yaml
@@ -0,0 +1,28 @@
+#
+# 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.
+#
+
+# Example: Basic Hello World action with Open API Specification
+project:
+  config: open_api_spec.json
+  packages:
+    hello_world_package:
+      version: 1.0
+      license: Apache-2.0
+      actions:
+        hello_world:
+          function: src/hello.js
+          web-export: true
diff --git a/docs/examples/open_api_spec.json b/docs/examples/open_api_spec.json
new file mode 100644
index 0000000..94aa099
--- /dev/null
+++ b/docs/examples/open_api_spec.json
@@ -0,0 +1,60 @@
+{
+    "swagger": "2.0",
+    "info": {
+        "version": "1.0",
+        "title": "Hello World API"
+    },
+    "basePath": "/hello",
+    "schemes": [
+        "https"
+    ],
+    "consumes": [
+        "application/json"
+    ],
+    "produces": [
+        "application/json"
+    ],
+    "paths": {
+        "/world": {
+            "get": {
+                "description": "Returns a greeting to the user!",
+                "operationId": "hello_world",
+                "responses": {
+                    "200": {
+                        "description": "Returns the greeting.",
+                        "schema": {
+                            "type": "string"
+                        }
+                    }
+                }
+            }
+        }
+
+    },
+    "x-gateway-configuration": {
+    "assembly": {
+      "execute": [
+        {
+          "operation-switch": {
+            "case": [
+              {
+                "operations": [
+                  "getHello"
+                ],
+                "execute": [
+                  {
+                    "invoke": {
+                      "target-url": "https://openwhisk.ng.bluemix.net/api/some/action/path.http",
+                      "verb": "keep"
+                    }
+                  }
+                ]
+              }
+            ],
+            "otherwise": []
+          }
+        }
+      ]
+    }
+  }
+}
diff --git a/docs/wskdeploy_apigateway_open_api_spec.md b/docs/wskdeploy_apigateway_open_api_spec.md
new file mode 100644
index 0000000..4942ed2
--- /dev/null
+++ b/docs/wskdeploy_apigateway_open_api_spec.md
@@ -0,0 +1,177 @@
+<!--
+#
+# 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.
+#
+-->
+
+# API Open API Spec
+
+## The "Hello World" API
+
+This example builds on the ["Hello World" Action](wskdeploy_action_helloworld.md#actions) example and ["Hello World" API](wdkdeploy_apigateway_helloworld.md) example by adding an Open API Specification to define an API that is invoked via an API call.
+
+It shows how to:
+- update the Action named ‘hello_world’ to expose it to the gateway.
+- specify the API's endpoint that will trigger the action via an Open API Specification.
+
+### Manifest file
+#### _Example: “Hello world” action with project config to point to the API spec_
+```yaml
+project:
+  config: open_api_spec.json
+  packages:
+    hello_world_package:
+      version: 1.0
+      license: Apache-2.0
+      actions:
+        hello_world:
+          function: src/hello.js
+          web-export: true
+```
+### Open API Specification
+#### _Example: "Hello world" open api specification_
+```json
+{
+    "swagger": "2.0",
+    "info": {
+        "version": "1.0",
+        "title": "Hello World API"
+    },
+    "basePath": "/hello",
+    "schemes": [
+        "https"
+    ],
+    "consumes": [
+        "application/json"
+    ],
+    "produces": [
+        "application/json"
+    ],
+    "paths": {
+        "/world": {
+            "get": {
+                "description": "Returns a greeting to the user!",
+                "operationId": "hello_world",
+                "responses": {
+                    "200": {
+                        "description": "Returns the greeting.",
+                        "schema": {
+                            "type": "string"
+                        }
+                    }
+                }
+            }
+        }
+
+    },
+    "x-gateway-configuration": {
+    "assembly": {
+      "execute": [
+        {
+          "operation-switch": {
+            "case": [
+              {
+                "operations": [
+                  "getHello"
+                ],
+                "execute": [
+                  {
+                    "invoke": {
+                      "target-url": "https://openwhisk.ng.bluemix.net/api/some/action/path.http",
+                      "verb": "keep"
+                    }
+                  }
+                ]
+              }
+            ],
+            "otherwise": []
+          }
+        }
+      ]
+    }
+  }
+}
+```
+*NOTE*: Some providers such as IBM may have changed these keys. For example, IBM uses `x-ibm-configuration` instead of `x-gateway-configuration`.
+
+There are two major differences from _"Hello World" API_ example:
+- the root key is now project as the open api specification is a project wide concept.
+- a new `config` key specifying where the Open API Specification is located.
+
+The `config` key under `project` in the manifest file specifies where the Open API Specification is located. The keyword `config` was chosen to remain consistent with the `config-file` terminology in OpenWhisk CLI flag option. The Open API Specification describes in a JSON document the the base path, endpoint, HTTP verb, and other details describing the API. For example, the document above describes a GET endpoint at `/hello/world` that recieves JSON as input and returns JSON as output.
+
+### Deploying
+
+You cannot deploy the "hello world API gateway Open API Specification" manifest from the openwhisk-wskdeploy project directory directly. You need to update the `target-url`. This valued will be specific to your deployment/provider. An example of such a URL would be: `https://us-south.functions.cloud.ibm.com/api/v1/web/jdoe@ibm.com/hello_world_package/hello_world.json`. After filling out that value, you would deploy via:
+
+```sh
+$ wskdeploy -m docs/examples/manifest_hello_world_apigateway_open_api_spec.yaml
+```
+
+### Invoking
+
+Check the full URL of your API first:
+```sh
+$ wsk api list
+```
+
+This will return some information on the API, including the full URL, which
+should end with `hello/world`. It can then be invoked:
+
+```sh
+$ curl <url>
+```
+
+### Result
+The invocation should return a JSON response that includes this result:
+
+```json
+{
+    "greeting": "Hello, undefined from undefined"
+}
+```
+
+The output parameter '```greeting```' contains "_undefined_" values for the '```name```' and '```place```' input parameters as they were not provided in the manifest or the HTTP call. You can provide them as query parameters:
+
+```sh
+$ curl <url>?name=World&place=Earth
+```
+
+### Discussion
+
+This "hello world" example represents the minimum valid Manifest file which includes only the required parts of the Project, Package, Action and Open API Specification location.
+
+### Source code
+The source code for the manifest and JavaScript files can be found here:
+- [manifest_hello_world_apigateway.yaml](examples/manifest_hello_world_apigateway_open_api_spec.yaml)
+- [open_api_spec.json](examples/open_api_spec.json)
+- [hello.js](examples/src/hello.js)
+
+---
+<!--
+ Bottom Navigation
+-->
+<html>
+<div align="center">
+<table align="center">
+  <tr>
+    <td><a href="wskdeploy_triggerrule_trigger_bindings.md#triggers-and-rules">&lt;&lt;&nbsp;previous</a></td>
+    <td><a href="programming_guide.md#guided-examples">Example Index</a></td>
+    <td><a href="wskdeploy_apigateway_sequence.md#api-gateway-sequence">next&nbsp;&gt;&gt;</a></td>
+  </tr>
+</table>
+</div>
+</html>
diff --git a/parsers/manifest_parser.go b/parsers/manifest_parser.go
index e25cc49..e7218e7 100644
--- a/parsers/manifest_parser.go
+++ b/parsers/manifest_parser.go
@@ -19,6 +19,7 @@
 
 import (
 	"encoding/base64"
+	"encoding/json"
 	"errors"
 	"io/ioutil"
 	"os"
@@ -38,6 +39,7 @@
 	"github.com/apache/openwhisk-wskdeploy/wskenv"
 	"github.com/apache/openwhisk-wskdeploy/wski18n"
 	"github.com/apache/openwhisk-wskdeploy/wskprint"
+	yamlHelper "github.com/ghodss/yaml"
 	"net/url"
 )
 
@@ -54,6 +56,9 @@
 	PARAM_CLOSING_BRACKET = "}"
 
 	DUMMY_APIGW_ACCESS_TOKEN = "DUMMY TOKEN"
+
+	YAML_FILE_EXTENSION = "yaml"
+	YML_FILE_EXTENSION  = "yml"
 )
 
 // Read existing manifest file or create new if none exists
@@ -1299,6 +1304,111 @@
 	return requests, requestOptions, nil
 }
 
+func (dm *YAMLParser) ComposeApiRecordsFromSwagger(client *whisk.Config, manifest *YAML) (*whisk.ApiCreateRequest, *whisk.ApiCreateRequestOptions, error) {
+	var api *whisk.Api
+	var err error
+	var config string = manifest.Project.Config
+	// Check to make sure the manifest has a config under project
+	if config == "" {
+		return nil, nil, nil
+	}
+
+	api, err = dm.parseSwaggerApi(config, client.Namespace)
+	if err != nil {
+		whisk.Debug(whisk.DbgError, "parseSwaggerApi() error: %s\n", err)
+		errMsg := wski18n.T("Unable to parse swagger file: {{.err}}", map[string]interface{}{"err": err})
+		whiskErr := whisk.MakeWskErrorFromWskError(errors.New(errMsg), err, whisk.EXIT_CODE_ERR_GENERAL,
+			whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE)
+		return nil, nil, whiskErr
+	}
+
+	apiCreateReq := new(whisk.ApiCreateRequest)
+	apiCreateReq.ApiDoc = api
+
+	apiCreateReqOptions := new(whisk.ApiCreateRequestOptions)
+
+	return apiCreateReq, apiCreateReqOptions, nil
+
+}
+
+/*
+ * Read the swagger config provided by under
+ * Project:
+ *   config: swagger_filename.[yaml|yml]
+ *
+ * NOTE: This was lifted almost verbatim from openwhisk-cli/commands/api.go
+ *       and as a follow up should probably be moved and updated to live in whiskclient-go
+ * NOTE: This does notb verify that actions used in swagger api definition are defined in
+ *        the openwhisk server or manifest.
+ */
+func (dm *YAMLParser) parseSwaggerApi(configfile, namespace string) (*whisk.Api, error) {
+	if len(configfile) == 0 {
+		whisk.Debug(whisk.DbgError, "No swagger file is specified\n")
+		errMsg := wski18n.T("A swagger configuration file was not specified.")
+		whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_GENERAL,
+			whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE)
+		return nil, whiskErr
+	}
+
+	swagger, err := ioutil.ReadFile(configfile)
+	if err != nil {
+		whisk.Debug(whisk.DbgError, "readFile(%s) error: %s\n", configfile, err)
+		errMsg := wski18n.T("Error reading swagger file '{{.name}}': {{.err}}",
+			map[string]interface{}{"name": configfile, "err": err})
+		whiskErr := whisk.MakeWskErrorFromWskError(errors.New(errMsg), err, whisk.EXIT_CODE_ERR_GENERAL,
+			whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE)
+		return nil, whiskErr
+	}
+
+	// Check if this swagger is in JSON or YAML format
+	isYaml := strings.HasSuffix(configfile, YAML_FILE_EXTENSION) || strings.HasSuffix(configfile, YML_FILE_EXTENSION)
+	if isYaml {
+		whisk.Debug(whisk.DbgInfo, "Converting YAML formated API configuration into JSON\n")
+		jsonbytes, err := yamlHelper.YAMLToJSON([]byte(swagger))
+		if err != nil {
+			whisk.Debug(whisk.DbgError, "yaml.YAMLToJSON() error: %s\n", err)
+			errMsg := wski18n.T("Unable to parse YAML configuration file: {{.err}}", map[string]interface{}{"err": err})
+			whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_GENERAL,
+				whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
+			return nil, whiskErr
+		}
+		swagger = jsonbytes
+	}
+
+	// Parse the JSON into a swagger object
+	swaggerObj := new(whisk.ApiSwagger)
+	err = json.Unmarshal([]byte(swagger), swaggerObj)
+	if err != nil {
+		whisk.Debug(whisk.DbgError, "JSON parse of '%s' error: %s\n", configfile, err)
+		errMsg := wski18n.T("Error parsing swagger file '{{.name}}': {{.err}}",
+			map[string]interface{}{"name": configfile, "err": err})
+		whiskErr := whisk.MakeWskErrorFromWskError(errors.New(errMsg), err, whisk.EXIT_CODE_ERR_GENERAL,
+			whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE)
+		return nil, whiskErr
+	}
+	if swaggerObj.BasePath == "" || swaggerObj.SwaggerName == "" || swaggerObj.Info == nil || swaggerObj.Paths == nil {
+		whisk.Debug(whisk.DbgError, "Swagger file is invalid.\n")
+		errMsg := wski18n.T("Swagger file is invalid (missing basePath, info, paths, or swagger fields)")
+		whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_GENERAL,
+			whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE)
+		return nil, whiskErr
+	}
+
+	if !strings.HasPrefix(swaggerObj.BasePath, "/") {
+		whisk.Debug(whisk.DbgError, "Swagger file basePath is invalid.\n")
+		errMsg := wski18n.T("Swagger file basePath must start with a leading slash (/)")
+		whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_GENERAL,
+			whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE)
+		return nil, whiskErr
+	}
+
+	api := new(whisk.Api)
+	api.Namespace = namespace
+	api.Swagger = string(swagger)
+
+	return api, nil
+}
+
 func (dm *YAMLParser) getGatewayMethods() []string {
 	methods := []string{}
 	for k := range whisk.ApiVerbs {
diff --git a/parsers/yamlparser.go b/parsers/yamlparser.go
index 707a361..cb85709 100644
--- a/parsers/yamlparser.go
+++ b/parsers/yamlparser.go
@@ -233,6 +233,7 @@
 	Version          string               `yaml:"version"`
 	Packages         map[string]Package   `yaml:"packages"`
 	Inputs           map[string]Parameter `yaml: parameters`
+	Config           string               `yaml:"config"`
 }
 
 type YAML struct {