github.com/mweagle/Sparta@v1.15.0/apigateway.go (about)

     1  package sparta
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"net/http"
     7  	"strconv"
     8  	"strings"
     9  
    10  	"github.com/aws/aws-sdk-go/aws"
    11  	"github.com/aws/aws-sdk-go/aws/session"
    12  	"github.com/aws/aws-sdk-go/service/apigateway"
    13  	gocf "github.com/mweagle/go-cloudformation"
    14  	"github.com/sirupsen/logrus"
    15  )
    16  
    17  // APIGateway repreents a type of API Gateway provisoining that can be exported
    18  type APIGateway interface {
    19  	LogicalResourceName() string
    20  	Marshal(serviceName string,
    21  		session *session.Session,
    22  		S3Bucket string,
    23  		S3Key string,
    24  		S3Version string,
    25  		roleNameMap map[string]*gocf.StringExpr,
    26  		template *gocf.Template,
    27  		noop bool,
    28  		logger *logrus.Logger) error
    29  	Describe(targetNodeName string) (*DescriptionInfo, error)
    30  }
    31  
    32  var defaultCORSHeaders = map[string]interface{}{
    33  	"Access-Control-Allow-Headers": "Content-Type,X-Amz-Date,Authorization,X-Api-Key",
    34  	"Access-Control-Allow-Methods": "*",
    35  	"Access-Control-Allow-Origin":  "*",
    36  }
    37  
    38  const (
    39  	// OutputAPIGatewayURL is the keyname used in the CloudFormation Output
    40  	// that stores the APIGateway provisioned URL
    41  	// @enum OutputKey
    42  	OutputAPIGatewayURL = "APIGatewayURL"
    43  )
    44  
    45  func corsMethodResponseParams(api *API) map[string]bool {
    46  
    47  	var userDefinedHeaders map[string]interface{}
    48  	if api != nil &&
    49  		api.CORSOptions != nil {
    50  		userDefinedHeaders = api.CORSOptions.Headers
    51  	}
    52  	if len(userDefinedHeaders) <= 0 {
    53  		userDefinedHeaders = defaultCORSHeaders
    54  	}
    55  	responseParams := make(map[string]bool)
    56  	for eachHeader := range userDefinedHeaders {
    57  		keyName := fmt.Sprintf("method.response.header.%s", eachHeader)
    58  		responseParams[keyName] = true
    59  	}
    60  	return responseParams
    61  }
    62  
    63  func corsIntegrationResponseParams(api *API) map[string]interface{} {
    64  
    65  	var userDefinedHeaders map[string]interface{}
    66  	if api != nil &&
    67  		api.CORSOptions != nil {
    68  		userDefinedHeaders = api.CORSOptions.Headers
    69  	}
    70  	if len(userDefinedHeaders) <= 0 {
    71  		userDefinedHeaders = defaultCORSHeaders
    72  	}
    73  	responseParams := make(map[string]interface{})
    74  	for eachHeader, eachHeaderValue := range userDefinedHeaders {
    75  		keyName := fmt.Sprintf("method.response.header.%s", eachHeader)
    76  		switch headerVal := eachHeaderValue.(type) {
    77  		case *gocf.StringExpr:
    78  			responseParams[keyName] = gocf.Join("",
    79  				gocf.String("'"),
    80  				headerVal.String(),
    81  				gocf.String("'"))
    82  		default:
    83  			responseParams[keyName] = fmt.Sprintf("'%s'", eachHeaderValue)
    84  		}
    85  	}
    86  	return responseParams
    87  }
    88  
    89  // DefaultMethodResponses returns the default set of Method HTTPStatus->Response
    90  // pass through responses.  The successfulHTTPStatusCode param is the single
    91  // 2XX response code to use for the method.
    92  func methodResponses(api *API, userResponses map[int]*Response, corsEnabled bool) *gocf.APIGatewayMethodMethodResponseList {
    93  
    94  	var responses gocf.APIGatewayMethodMethodResponseList
    95  	for eachHTTPStatusCode, eachResponse := range userResponses {
    96  		methodResponseParams := eachResponse.Parameters
    97  		if corsEnabled {
    98  			for eachString, eachBool := range corsMethodResponseParams(api) {
    99  				methodResponseParams[eachString] = eachBool
   100  			}
   101  		}
   102  		// Then transform them all to strings because internet
   103  		methodResponseStringParams := make(map[string]string, len(methodResponseParams))
   104  		for eachKey, eachBool := range methodResponseParams {
   105  			methodResponseStringParams[eachKey] = fmt.Sprintf("%t", eachBool)
   106  		}
   107  		methodResponse := gocf.APIGatewayMethodMethodResponse{
   108  			StatusCode: gocf.String(strconv.Itoa(eachHTTPStatusCode)),
   109  		}
   110  		if len(methodResponseStringParams) != 0 {
   111  			methodResponse.ResponseParameters = methodResponseStringParams
   112  		}
   113  		responses = append(responses, methodResponse)
   114  	}
   115  	return &responses
   116  }
   117  
   118  func integrationResponses(api *API, userResponses map[int]*IntegrationResponse, corsEnabled bool) *gocf.APIGatewayMethodIntegrationResponseList {
   119  
   120  	var integrationResponses gocf.APIGatewayMethodIntegrationResponseList
   121  
   122  	// We've already populated this entire map in the NewMethod call
   123  	for eachHTTPStatusCode, eachMethodIntegrationResponse := range userResponses {
   124  		responseParameters := eachMethodIntegrationResponse.Parameters
   125  		if corsEnabled {
   126  			for eachKey, eachValue := range corsIntegrationResponseParams(api) {
   127  				responseParameters[eachKey] = eachValue
   128  			}
   129  		}
   130  
   131  		integrationResponse := gocf.APIGatewayMethodIntegrationResponse{
   132  			ResponseTemplates: eachMethodIntegrationResponse.Templates,
   133  			SelectionPattern:  gocf.String(eachMethodIntegrationResponse.SelectionPattern),
   134  			StatusCode:        gocf.String(strconv.Itoa(eachHTTPStatusCode)),
   135  		}
   136  		if len(responseParameters) != 0 {
   137  			integrationResponse.ResponseParameters = responseParameters
   138  		}
   139  		integrationResponses = append(integrationResponses, integrationResponse)
   140  	}
   141  
   142  	return &integrationResponses
   143  }
   144  
   145  func methodRequestTemplates(method *Method) (map[string]string, error) {
   146  	supportedTemplates := map[string]string{
   147  		"application/json":                  _escFSMustString(false, "/resources/provision/apigateway/inputmapping_json.vtl"),
   148  		"text/plain":                        _escFSMustString(false, "/resources/provision/apigateway/inputmapping_default.vtl"),
   149  		"application/x-www-form-urlencoded": _escFSMustString(false, "/resources/provision/apigateway/inputmapping_formencoded.vtl"),
   150  		"multipart/form-data":               _escFSMustString(false, "/resources/provision/apigateway/inputmapping_default.vtl"),
   151  	}
   152  	if len(method.SupportedRequestContentTypes) <= 0 {
   153  		return supportedTemplates, nil
   154  	}
   155  
   156  	// Else, let's go ahead and return only the mappings the user wanted
   157  	userDefinedTemplates := make(map[string]string)
   158  	for _, eachContentType := range method.SupportedRequestContentTypes {
   159  		vtlMapping, vtlMappingExists := supportedTemplates[eachContentType]
   160  		if !vtlMappingExists {
   161  			return nil, fmt.Errorf("unsupported method request template Content-Type provided: %s", eachContentType)
   162  		}
   163  		userDefinedTemplates[eachContentType] = vtlMapping
   164  	}
   165  	return userDefinedTemplates, nil
   166  }
   167  
   168  func corsOptionsGatewayMethod(api *API, restAPIID gocf.Stringable, resourceID gocf.Stringable) *gocf.APIGatewayMethod {
   169  	methodResponse := gocf.APIGatewayMethodMethodResponse{
   170  		StatusCode:         gocf.String("200"),
   171  		ResponseParameters: corsMethodResponseParams(api),
   172  	}
   173  
   174  	integrationResponse := gocf.APIGatewayMethodIntegrationResponse{
   175  		ResponseTemplates: map[string]string{
   176  			"application/*": "",
   177  			"text/*":        "",
   178  		},
   179  		StatusCode:         gocf.String("200"),
   180  		ResponseParameters: corsIntegrationResponseParams(api),
   181  	}
   182  
   183  	methodIntegrationIntegrationResponseList := gocf.APIGatewayMethodIntegrationResponseList{}
   184  	methodIntegrationIntegrationResponseList = append(methodIntegrationIntegrationResponseList,
   185  		integrationResponse)
   186  	methodResponseList := gocf.APIGatewayMethodMethodResponseList{}
   187  	methodResponseList = append(methodResponseList, methodResponse)
   188  
   189  	corsMethod := &gocf.APIGatewayMethod{
   190  		HTTPMethod:        gocf.String("OPTIONS"),
   191  		AuthorizationType: gocf.String("NONE"),
   192  		RestAPIID:         restAPIID.String(),
   193  		ResourceID:        resourceID.String(),
   194  		Integration: &gocf.APIGatewayMethodIntegration{
   195  			Type: gocf.String("MOCK"),
   196  			RequestTemplates: map[string]string{
   197  				"application/json": "{\"statusCode\": 200}",
   198  				"text/plain":       "statusCode: 200",
   199  			},
   200  			IntegrationResponses: &methodIntegrationIntegrationResponseList,
   201  		},
   202  		MethodResponses: &methodResponseList,
   203  	}
   204  	return corsMethod
   205  }
   206  
   207  func apiStageInfo(apiName string,
   208  	stageName string,
   209  	session *session.Session,
   210  	noop bool,
   211  	logger *logrus.Logger) (*apigateway.Stage, error) {
   212  
   213  	logger.WithFields(logrus.Fields{
   214  		"APIName":   apiName,
   215  		"StageName": stageName,
   216  	}).Info("Checking current API Gateway stage status")
   217  
   218  	if noop {
   219  		logger.Info(noopMessage("API Gateway check"))
   220  		return nil, nil
   221  	}
   222  
   223  	svc := apigateway.New(session)
   224  	restApisInput := &apigateway.GetRestApisInput{
   225  		Limit: aws.Int64(500),
   226  	}
   227  
   228  	restApisOutput, restApisOutputErr := svc.GetRestApis(restApisInput)
   229  	if nil != restApisOutputErr {
   230  		return nil, restApisOutputErr
   231  	}
   232  	// Find the entry that has this name
   233  	restAPIID := ""
   234  	for _, eachRestAPI := range restApisOutput.Items {
   235  		if *eachRestAPI.Name == apiName {
   236  			if restAPIID != "" {
   237  				return nil, fmt.Errorf("multiple RestAPI matches for API Name: %s", apiName)
   238  			}
   239  			restAPIID = *eachRestAPI.Id
   240  		}
   241  	}
   242  	if restAPIID == "" {
   243  		return nil, nil
   244  	}
   245  	// API exists...does the stage name exist?
   246  	stagesInput := &apigateway.GetStagesInput{
   247  		RestApiId: aws.String(restAPIID),
   248  	}
   249  	stagesOutput, stagesOutputErr := svc.GetStages(stagesInput)
   250  	if nil != stagesOutputErr {
   251  		return nil, stagesOutputErr
   252  	}
   253  
   254  	// Find this stage name...
   255  	var matchingStageOutput *apigateway.Stage
   256  	for _, eachStage := range stagesOutput.Item {
   257  		if *eachStage.StageName == stageName {
   258  			if nil != matchingStageOutput {
   259  				return nil, fmt.Errorf("multiple stage matches for name: %s", stageName)
   260  			}
   261  			matchingStageOutput = eachStage
   262  		}
   263  	}
   264  	if nil != matchingStageOutput {
   265  		logger.WithFields(logrus.Fields{
   266  			"DeploymentId": *matchingStageOutput.DeploymentId,
   267  			"LastUpdated":  matchingStageOutput.LastUpdatedDate,
   268  			"CreatedDate":  matchingStageOutput.CreatedDate,
   269  		}).Info("Checking current APIGateway stage status")
   270  	} else {
   271  		logger.Info("APIGateway stage has not been deployed")
   272  	}
   273  	return matchingStageOutput, nil
   274  }
   275  
   276  ////////////////////////////////////////////////////////////////////////////////
   277  //
   278  
   279  // APIGatewayIdentity represents the user identity of a request
   280  // made on behalf of the API Gateway
   281  type APIGatewayIdentity struct {
   282  	// Account ID
   283  	AccountID string `json:"accountId"`
   284  	// API Key
   285  	APIKey string `json:"apiKey"`
   286  	// Caller
   287  	Caller string `json:"caller"`
   288  	// Cognito Authentication Provider
   289  	CognitoAuthenticationProvider string `json:"cognitoAuthenticationProvider"`
   290  	// Cognito Authentication Type
   291  	CognitoAuthenticationType string `json:"cognitoAuthenticationType"`
   292  	// CognitoIdentityId
   293  	CognitoIdentityID string `json:"cognitoIdentityId"`
   294  	// CognitoIdentityPoolId
   295  	CognitoIdentityPoolID string `json:"cognitoIdentityPoolId"`
   296  	// Source IP
   297  	SourceIP string `json:"sourceIp"`
   298  	// User
   299  	User string `json:"user"`
   300  	// User Agent
   301  	UserAgent string `json:"userAgent"`
   302  	// User ARN
   303  	UserARN string `json:"userArn"`
   304  }
   305  
   306  ////////////////////////////////////////////////////////////////////////////////
   307  //
   308  
   309  // APIGatewayContext represents the context available to an AWS Lambda
   310  // function that is invoked by an API Gateway integration.
   311  type APIGatewayContext struct {
   312  	// API ID
   313  	APIID string `json:"apiId"`
   314  	// HTTPMethod
   315  	Method string `json:"method"`
   316  	// Request ID
   317  	RequestID string `json:"requestId"`
   318  	// Resource ID
   319  	ResourceID string `json:"resourceId"`
   320  	// Resource Path
   321  	ResourcePath string `json:"resourcePath"`
   322  	// Stage
   323  	Stage string `json:"stage"`
   324  	// User identity
   325  	Identity APIGatewayIdentity `json:"identity"`
   326  }
   327  
   328  ////////////////////////////////////////////////////////////////////////////////
   329  //
   330  
   331  // APIGatewayLambdaJSONEvent provides a pass through mapping
   332  // of all whitelisted Parameters.  The transformation is defined
   333  // by the resources/gateway/inputmapping_json.vtl template.
   334  type APIGatewayLambdaJSONEvent struct {
   335  	// HTTPMethod
   336  	Method string `json:"method"`
   337  	// Body, if available
   338  	Body json.RawMessage `json:"body"`
   339  	// Whitelisted HTTP headers
   340  	Headers map[string]string `json:"headers"`
   341  	// Whitelisted HTTP query params
   342  	QueryParams map[string]string `json:"queryParams"`
   343  	// Whitelisted path parameters
   344  	PathParams map[string]string `json:"pathParams"`
   345  	// Context information - http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html#context-variable-reference
   346  	Context APIGatewayContext `json:"context"`
   347  }
   348  
   349  ////////////////////////////////////////////////////////////////////////////////
   350  //
   351  
   352  // Model proxies the AWS SDK's Model data.  See
   353  // http://docs.aws.amazon.com/sdk-for-go/api/service/apigateway.html#Model
   354  //
   355  // TODO: Support Dynamic Model creation
   356  type Model struct {
   357  	Description string `json:",omitempty"`
   358  	Name        string `json:",omitempty"`
   359  	Schema      string `json:",omitempty"`
   360  }
   361  
   362  ////////////////////////////////////////////////////////////////////////////////
   363  //
   364  
   365  // Response proxies the AWS SDK's PutMethodResponseInput data.  See
   366  // http://docs.aws.amazon.com/sdk-for-go/api/service/apigateway.html#PutMethodResponseInput
   367  type Response struct {
   368  	Parameters map[string]bool   `json:",omitempty"`
   369  	Models     map[string]*Model `json:",omitempty"`
   370  }
   371  
   372  ////////////////////////////////////////////////////////////////////////////////
   373  //
   374  
   375  // IntegrationResponse proxies the AWS SDK's IntegrationResponse data.  See
   376  // http://docs.aws.amazon.com/sdk-for-go/api/service/apigateway/#IntegrationResponse
   377  type IntegrationResponse struct {
   378  	Parameters       map[string]interface{} `json:",omitempty"`
   379  	SelectionPattern string                 `json:",omitempty"`
   380  	Templates        map[string]string      `json:",omitempty"`
   381  }
   382  
   383  ////////////////////////////////////////////////////////////////////////////////
   384  //
   385  
   386  // Integration proxies the AWS SDK's Integration data.  See
   387  // http://docs.aws.amazon.com/sdk-for-go/api/service/apigateway.html#Integration
   388  type Integration struct {
   389  	Parameters         map[string]string
   390  	RequestTemplates   map[string]string
   391  	CacheKeyParameters []string
   392  	CacheNamespace     string
   393  	Credentials        string
   394  
   395  	Responses map[int]*IntegrationResponse
   396  
   397  	// Typically "AWS", but for OPTIONS CORS support is set to "MOCK"
   398  	integrationType string
   399  }
   400  
   401  ////////////////////////////////////////////////////////////////////////////////
   402  //
   403  
   404  // Method proxies the AWS SDK's Method data.  See
   405  // http://docs.aws.amazon.com/sdk-for-go/api/service/apigateway.html#type-Method
   406  type Method struct {
   407  	authorizationID         gocf.Stringable
   408  	httpMethod              string
   409  	defaultHTTPResponseCode int
   410  
   411  	APIKeyRequired bool
   412  
   413  	// Request data
   414  	Parameters map[string]bool
   415  	Models     map[string]*Model
   416  
   417  	// Supported HTTP request Content-Types. Used to limit the amount of VTL
   418  	// injected into the CloudFormation template. Eligible values include:
   419  	// application/json
   420  	// text/plain
   421  	// application/x-www-form-urlencoded
   422  	// multipart/form-data
   423  	SupportedRequestContentTypes []string
   424  
   425  	// Response map
   426  	Responses map[int]*Response
   427  
   428  	// Integration response map
   429  	Integration Integration
   430  }
   431  
   432  ////////////////////////////////////////////////////////////////////////////////
   433  //
   434  
   435  // Resource proxies the AWS SDK's Resource data.  See
   436  // http://docs.aws.amazon.com/sdk-for-go/api/service/apigateway.html#type-Resource
   437  type Resource struct {
   438  	pathPart     string
   439  	parentLambda *LambdaAWSInfo
   440  	Methods      map[string]*Method
   441  }
   442  
   443  // Stage proxies the AWS SDK's Stage data.  See
   444  // http://docs.aws.amazon.com/sdk-for-go/api/service/apigateway.html#type-Stage
   445  type Stage struct {
   446  	name                string
   447  	CacheClusterEnabled bool
   448  	CacheClusterSize    string
   449  	Description         string
   450  	Variables           map[string]string
   451  }
   452  
   453  ////////////////////////////////////////////////////////////////////////////////
   454  //
   455  
   456  // CORSOptions is a struct that clients supply to the API in order to enable
   457  // and parameterize CORS API values
   458  type CORSOptions struct {
   459  	// Headers represent the CORS headers that should be used for an OPTIONS
   460  	// preflight request. These should be of the form key-value as in:
   461  	// "Access-Control-Allow-Headers"="Content-Type,X-Amz-Date,Authorization,X-Api-Key"
   462  	Headers map[string]interface{}
   463  }
   464  
   465  ////////////////////////////////////////////////////////////////////////////////
   466  //
   467  
   468  // API represents the AWS API Gateway data associated with a given Sparta app.  Proxies
   469  // the AWS SDK's CreateRestApiInput data.  See
   470  // http://docs.aws.amazon.com/sdk-for-go/api/service/apigateway.html#type-CreateRestApiInput
   471  type API struct {
   472  	// The API name
   473  	// TODO: bind this to the stack name to prevent provisioning collisions.
   474  	name string
   475  	// Optional stage. If defined, the API will be deployed
   476  	stage *Stage
   477  	// Existing API to CloneFrom
   478  	CloneFrom string
   479  	// APIDescription is the user defined description
   480  	Description string
   481  	// Non-empty map of urlPaths->Resource definitions
   482  	resources map[string]*Resource
   483  	// Should CORS be enabled for this API?
   484  	CORSEnabled bool
   485  	// CORS options - if non-nil, supersedes CORSEnabled
   486  	CORSOptions *CORSOptions
   487  	// Endpoint configuration information
   488  	EndpointConfiguration *gocf.APIGatewayRestAPIEndpointConfiguration
   489  }
   490  
   491  // LogicalResourceName returns the CloudFormation logical
   492  // resource name for this API
   493  func (api *API) LogicalResourceName() string {
   494  	return CloudFormationResourceName("APIGateway", api.name)
   495  }
   496  
   497  // RestAPIURL returns the dynamically assigned
   498  // Rest API URL including the scheme
   499  func (api *API) RestAPIURL() *gocf.StringExpr {
   500  	return gocf.Join("",
   501  		gocf.String("https://"),
   502  		gocf.Ref(api.LogicalResourceName()),
   503  		gocf.String(".execute-api."),
   504  		gocf.Ref("AWS::Region"),
   505  		gocf.String(".amazonaws.com"))
   506  }
   507  
   508  func (api *API) corsEnabled() bool {
   509  	return api.CORSEnabled || (api.CORSOptions != nil)
   510  }
   511  
   512  // Describe returns the API for description
   513  func (api *API) Describe(targetNodeName string) (*DescriptionInfo, error) {
   514  	descInfo := &DescriptionInfo{
   515  		Name:  "APIGateway",
   516  		Nodes: make([]*DescriptionTriplet, 0),
   517  	}
   518  	descInfo.Nodes = append(descInfo.Nodes, &DescriptionTriplet{
   519  		SourceNodeName: nodeNameAPIGateway,
   520  		DisplayInfo: &DescriptionDisplayInfo{
   521  			SourceNodeColor: nodeColorAPIGateway,
   522  			SourceIcon: &DescriptionIcon{
   523  				Category: "Mobile",
   524  				Name:     "Amazon-API-Gateway_light-bg@4x.png",
   525  			},
   526  		},
   527  		TargetNodeName: targetNodeName,
   528  	})
   529  
   530  	// Create the APIGateway virtual node && connect it to the application
   531  	for _, eachResource := range api.resources {
   532  		for eachMethod := range eachResource.Methods {
   533  			// Create the PATH node
   534  			var nodeName = fmt.Sprintf("%s - %s", eachMethod, eachResource.pathPart)
   535  			descInfo.Nodes = append(descInfo.Nodes,
   536  				&DescriptionTriplet{
   537  					SourceNodeName: nodeName,
   538  					DisplayInfo: &DescriptionDisplayInfo{
   539  						SourceNodeColor: nodeColorAPIGateway,
   540  						SourceIcon: &DescriptionIcon{
   541  							Category: "_General",
   542  							Name:     "Internet-alt1_light-bg@4x.png",
   543  						},
   544  					},
   545  					TargetNodeName: nodeNameAPIGateway,
   546  				},
   547  				&DescriptionTriplet{
   548  					SourceNodeName: nodeName,
   549  					TargetNodeName: eachResource.parentLambda.lambdaFunctionName(),
   550  				})
   551  		}
   552  	}
   553  	return descInfo, nil
   554  }
   555  
   556  // Marshal marshals the API data to a CloudFormation compatible representation
   557  func (api *API) Marshal(serviceName string,
   558  	session *session.Session,
   559  	S3Bucket string,
   560  	S3Key string,
   561  	S3Version string,
   562  	roleNameMap map[string]*gocf.StringExpr,
   563  	template *gocf.Template,
   564  	noop bool,
   565  	logger *logrus.Logger) error {
   566  
   567  	apiGatewayResourceNameForPath := func(fullPath string) string {
   568  		pathParts := strings.Split(fullPath, "/")
   569  		return CloudFormationResourceName("%sResource", pathParts[0], fullPath)
   570  	}
   571  
   572  	// Create an API gateway entry
   573  	apiGatewayRes := &gocf.APIGatewayRestAPI{
   574  		Description:    gocf.String(api.Description),
   575  		FailOnWarnings: gocf.Bool(false),
   576  		Name:           gocf.String(api.name),
   577  	}
   578  	if api.CloneFrom != "" {
   579  		apiGatewayRes.CloneFrom = gocf.String(api.CloneFrom)
   580  	}
   581  	if api.Description == "" {
   582  		apiGatewayRes.Description = gocf.String(fmt.Sprintf("%s RestApi", serviceName))
   583  	} else {
   584  		apiGatewayRes.Description = gocf.String(api.Description)
   585  	}
   586  	apiGatewayResName := api.LogicalResourceName()
   587  	// Is there an endpoint type?
   588  	if api.EndpointConfiguration != nil {
   589  		apiGatewayRes.EndpointConfiguration = api.EndpointConfiguration
   590  	}
   591  	template.AddResource(apiGatewayResName, apiGatewayRes)
   592  	apiGatewayRestAPIID := gocf.Ref(apiGatewayResName)
   593  
   594  	// List of all the method resources we're creating s.t. the
   595  	// deployment can DependOn them
   596  	optionsMethodPathMap := make(map[string]bool)
   597  	var apiMethodCloudFormationResources []string
   598  	for eachResourceMethodKey, eachResourceDef := range api.resources {
   599  		// First walk all the user resources and create intermediate paths
   600  		// to repreesent all the resources
   601  		var parentResource *gocf.StringExpr
   602  		pathParts := strings.Split(strings.TrimLeft(eachResourceDef.pathPart, "/"), "/")
   603  		pathAccumulator := []string{"/"}
   604  		for index, eachPathPart := range pathParts {
   605  			pathAccumulator = append(pathAccumulator, eachPathPart)
   606  			resourcePathName := apiGatewayResourceNameForPath(strings.Join(pathAccumulator, "/"))
   607  			if _, exists := template.Resources[resourcePathName]; !exists {
   608  				cfResource := &gocf.APIGatewayResource{
   609  					RestAPIID: apiGatewayRestAPIID.String(),
   610  					PathPart:  gocf.String(eachPathPart),
   611  				}
   612  				if index <= 0 {
   613  					cfResource.ParentID = gocf.GetAtt(apiGatewayResName, "RootResourceId")
   614  				} else {
   615  					cfResource.ParentID = parentResource
   616  				}
   617  				template.AddResource(resourcePathName, cfResource)
   618  			}
   619  			parentResource = gocf.Ref(resourcePathName).String()
   620  		}
   621  
   622  		// Add the lambda permission
   623  		apiGatewayPermissionResourceName := CloudFormationResourceName("APIGatewayLambdaPerm",
   624  			eachResourceMethodKey)
   625  		lambdaInvokePermission := &gocf.LambdaPermission{
   626  			Action:       gocf.String("lambda:InvokeFunction"),
   627  			FunctionName: gocf.GetAtt(eachResourceDef.parentLambda.LogicalResourceName(), "Arn"),
   628  			Principal:    gocf.String(APIGatewayPrincipal),
   629  		}
   630  		template.AddResource(apiGatewayPermissionResourceName, lambdaInvokePermission)
   631  
   632  		// BEGIN CORS - OPTIONS verb
   633  		// CORS is API global, but it's possible that there are multiple different lambda functions
   634  		// that are handling the same HTTP resource. In this case, track whether we've already created an
   635  		// OPTIONS entry for this path and only append iff this is the first time through
   636  		if api.corsEnabled() {
   637  			methodResourceName := CloudFormationResourceName(fmt.Sprintf("%s-OPTIONS",
   638  				eachResourceDef.pathPart), eachResourceDef.pathPart)
   639  			_, resourceExists := optionsMethodPathMap[methodResourceName]
   640  			if !resourceExists {
   641  				template.AddResource(methodResourceName, corsOptionsGatewayMethod(api,
   642  					apiGatewayRestAPIID,
   643  					parentResource))
   644  				apiMethodCloudFormationResources = append(apiMethodCloudFormationResources, methodResourceName)
   645  				optionsMethodPathMap[methodResourceName] = true
   646  			}
   647  		}
   648  		// END CORS - OPTIONS verb
   649  
   650  		// BEGIN - user defined verbs
   651  		for eachMethodName, eachMethodDef := range eachResourceDef.Methods {
   652  
   653  			methodRequestTemplates, methodRequestTemplatesErr := methodRequestTemplates(eachMethodDef)
   654  			if methodRequestTemplatesErr != nil {
   655  				return methodRequestTemplatesErr
   656  			}
   657  			apiGatewayMethod := &gocf.APIGatewayMethod{
   658  				HTTPMethod: gocf.String(eachMethodName),
   659  				ResourceID: parentResource.String(),
   660  				RestAPIID:  apiGatewayRestAPIID.String(),
   661  				Integration: &gocf.APIGatewayMethodIntegration{
   662  					IntegrationHTTPMethod: gocf.String("POST"),
   663  					Type:                  gocf.String("AWS"),
   664  					RequestTemplates:      methodRequestTemplates,
   665  					URI: gocf.Join("",
   666  						gocf.String("arn:aws:apigateway:"),
   667  						gocf.Ref("AWS::Region"),
   668  						gocf.String(":lambda:path/2015-03-31/functions/"),
   669  						gocf.GetAtt(eachResourceDef.parentLambda.LogicalResourceName(), "Arn"),
   670  						gocf.String("/invocations")),
   671  				},
   672  			}
   673  			// Handle authorization
   674  			if eachMethodDef.authorizationID != nil {
   675  				// See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-method.html#cfn-apigateway-method-authorizationtype
   676  				apiGatewayMethod.AuthorizationType = gocf.String("CUSTOM")
   677  				apiGatewayMethod.AuthorizerID = eachMethodDef.authorizationID.String()
   678  			} else {
   679  				apiGatewayMethod.AuthorizationType = gocf.String("NONE")
   680  			}
   681  			if len(eachMethodDef.Parameters) != 0 {
   682  				requestParams := make(map[string]string)
   683  				for eachKey, eachBool := range eachMethodDef.Parameters {
   684  					requestParams[eachKey] = fmt.Sprintf("%t", eachBool)
   685  				}
   686  				apiGatewayMethod.RequestParameters = requestParams
   687  			}
   688  
   689  			// Add the integration response RegExps
   690  			apiGatewayMethod.Integration.IntegrationResponses = integrationResponses(api,
   691  				eachMethodDef.Integration.Responses,
   692  				api.corsEnabled())
   693  
   694  			// Add outbound method responses
   695  			apiGatewayMethod.MethodResponses = methodResponses(api,
   696  				eachMethodDef.Responses,
   697  				api.corsEnabled())
   698  
   699  			prefix := fmt.Sprintf("%s%s", eachMethodDef.httpMethod, eachResourceMethodKey)
   700  			methodResourceName := CloudFormationResourceName(prefix, eachResourceMethodKey, serviceName)
   701  			res := template.AddResource(methodResourceName, apiGatewayMethod)
   702  			res.DependsOn = append(res.DependsOn, apiGatewayPermissionResourceName)
   703  			apiMethodCloudFormationResources = append(apiMethodCloudFormationResources,
   704  				methodResourceName)
   705  		}
   706  	}
   707  	// END
   708  	if nil != api.stage {
   709  		// Is the stack already deployed?
   710  		stageName := api.stage.name
   711  		stageInfo, stageInfoErr := apiStageInfo(api.name,
   712  			stageName,
   713  			session,
   714  			noop,
   715  			logger)
   716  		if nil != stageInfoErr {
   717  			return stageInfoErr
   718  		}
   719  		if nil == stageInfo {
   720  			// Use a stable identifier so that we can update the existing deployment
   721  			apiDeploymentResName := CloudFormationResourceName("APIGatewayDeployment",
   722  				serviceName)
   723  			apiDeployment := &gocf.APIGatewayDeployment{
   724  				Description: gocf.String(api.stage.Description),
   725  				RestAPIID:   apiGatewayRestAPIID.String(),
   726  				StageName:   gocf.String(stageName),
   727  				StageDescription: &gocf.APIGatewayDeploymentStageDescription{
   728  					Description: gocf.String(api.stage.Description),
   729  					Variables:   api.stage.Variables,
   730  				},
   731  			}
   732  			if api.stage.CacheClusterEnabled {
   733  				apiDeployment.StageDescription.CacheClusterEnabled =
   734  					gocf.Bool(api.stage.CacheClusterEnabled)
   735  			}
   736  			if api.stage.CacheClusterSize != "" {
   737  				apiDeployment.StageDescription.CacheClusterSize =
   738  					gocf.String(api.stage.CacheClusterSize)
   739  			}
   740  			deployment := template.AddResource(apiDeploymentResName, apiDeployment)
   741  			deployment.DependsOn = append(deployment.DependsOn, apiMethodCloudFormationResources...)
   742  			deployment.DependsOn = append(deployment.DependsOn, apiGatewayResName)
   743  		} else {
   744  			newDeployment := &gocf.APIGatewayDeployment{
   745  				Description: gocf.String("Deployment"),
   746  				RestAPIID:   apiGatewayRestAPIID.String(),
   747  			}
   748  			if stageInfo.StageName != nil {
   749  				newDeployment.StageName = gocf.String(*stageInfo.StageName)
   750  			}
   751  			// Use an unstable ID s.t. we can actually create a new deployment event.  Not sure how this
   752  			// is going to work with deletes...
   753  			deploymentResName := CloudFormationResourceName("APIGatewayDeployment")
   754  			deployment := template.AddResource(deploymentResName, newDeployment)
   755  			deployment.DependsOn = append(deployment.DependsOn, apiMethodCloudFormationResources...)
   756  			deployment.DependsOn = append(deployment.DependsOn, apiGatewayResName)
   757  		}
   758  		// Outputs...
   759  		template.Outputs[OutputAPIGatewayURL] = &gocf.Output{
   760  			Description: "API Gateway URL",
   761  			Value: gocf.Join("",
   762  				gocf.String("https://"),
   763  				apiGatewayRestAPIID,
   764  				gocf.String(".execute-api."),
   765  				gocf.Ref("AWS::Region"),
   766  				gocf.String(".amazonaws.com/"),
   767  				gocf.String(stageName)),
   768  		}
   769  	}
   770  	return nil
   771  }
   772  
   773  // NewAPIGateway returns a new API Gateway structure.  If stage is defined, the API Gateway
   774  // will also be deployed as part of stack creation.
   775  func NewAPIGateway(name string, stage *Stage) *API {
   776  	return &API{
   777  		name:        name,
   778  		stage:       stage,
   779  		resources:   make(map[string]*Resource),
   780  		CORSEnabled: false,
   781  		CORSOptions: nil,
   782  	}
   783  }
   784  
   785  // NewStage returns a Stage object with the given name.  Providing a Stage value
   786  // to NewAPIGateway implies that the API Gateway resources should be deployed
   787  // (eg: made publicly accessible).  See
   788  // http://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-deploy-api.html
   789  func NewStage(name string) *Stage {
   790  	return &Stage{
   791  		name:      name,
   792  		Variables: make(map[string]string),
   793  	}
   794  }
   795  
   796  // NewResource associates a URL path value with the LambdaAWSInfo golang lambda.  To make
   797  // the Resource available, associate one or more Methods via NewMethod().
   798  func (api *API) NewResource(pathPart string, parentLambda *LambdaAWSInfo) (*Resource, error) {
   799  	// The key is the path+resource, since we want to support POLA scoped
   800  	// security roles based on HTTP method
   801  	resourcesKey := fmt.Sprintf("%s%s", parentLambda.lambdaFunctionName(), pathPart)
   802  	_, exists := api.resources[resourcesKey]
   803  	if exists {
   804  		return nil, fmt.Errorf("path %s already defined for lambda function: %s", pathPart, parentLambda.lambdaFunctionName())
   805  	}
   806  	resource := &Resource{
   807  		pathPart:     pathPart,
   808  		parentLambda: parentLambda,
   809  		Methods:      make(map[string]*Method),
   810  	}
   811  	api.resources[resourcesKey] = resource
   812  	return resource, nil
   813  }
   814  
   815  // NewMethod associates the httpMethod name with the given Resource.  The returned Method
   816  // has no authorization requirements. To limit the amount of API gateway resource mappings,
   817  // supply the variadic slice of  possibleHTTPStatusCodeResponses which is the universe
   818  // of all HTTP status codes returned by your Sparta function. If this slice is non-empty,
   819  // Sparta will *ONLY* generate mappings for known codes. This slice need only include the
   820  // codes in addition to the defaultHTTPStatusCode. If the function can only return a single
   821  // value, provide the defaultHTTPStatusCode in the possibleHTTPStatusCodeResponses slice
   822  func (resource *Resource) NewMethod(httpMethod string,
   823  	defaultHTTPStatusCode int,
   824  	possibleHTTPStatusCodeResponses ...int) (*Method, error) {
   825  
   826  	if OptionsGlobal.Logger != nil && len(possibleHTTPStatusCodeResponses) != 0 {
   827  		OptionsGlobal.Logger.WithFields(logrus.Fields{
   828  			"possibleHTTPStatusCodeResponses": possibleHTTPStatusCodeResponses,
   829  		}).Debug("The set of all HTTP status codes is no longer required for NewMethod(...). Any valid HTTP status code can be returned starting with v1.8.0.")
   830  	}
   831  
   832  	// http://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-method-settings.html#how-to-method-settings-console
   833  	keyname := httpMethod
   834  	existingMethod, exists := resource.Methods[keyname]
   835  	if exists {
   836  		return nil, fmt.Errorf("method %s (Auth: %#v) already defined for resource",
   837  			httpMethod,
   838  			existingMethod.authorizationID)
   839  	}
   840  	if defaultHTTPStatusCode == 0 {
   841  		return nil, fmt.Errorf("invalid default HTTP status (%d) code for method", defaultHTTPStatusCode)
   842  	}
   843  
   844  	integration := Integration{
   845  		Parameters:       make(map[string]string),
   846  		RequestTemplates: make(map[string]string),
   847  		Responses:        make(map[int]*IntegrationResponse),
   848  		integrationType:  "AWS", // Type used for Lambda integration
   849  	}
   850  
   851  	method := &Method{
   852  		httpMethod:              httpMethod,
   853  		defaultHTTPResponseCode: defaultHTTPStatusCode,
   854  		Parameters:              make(map[string]bool),
   855  		Models:                  make(map[string]*Model),
   856  		Responses:               make(map[int]*Response),
   857  		Integration:             integration,
   858  	}
   859  
   860  	// Eligible HTTP status codes...
   861  	if len(possibleHTTPStatusCodeResponses) <= 0 {
   862  		// User didn't supply any potential codes, so use the entire set...
   863  		for i := http.StatusOK; i <= http.StatusNetworkAuthenticationRequired; i++ {
   864  			if len(http.StatusText(i)) != 0 {
   865  				possibleHTTPStatusCodeResponses = append(possibleHTTPStatusCodeResponses, i)
   866  			}
   867  		}
   868  	} else {
   869  		// There are some, so include them, plus the default one
   870  		possibleHTTPStatusCodeResponses = append(possibleHTTPStatusCodeResponses,
   871  			defaultHTTPStatusCode)
   872  	}
   873  
   874  	// So we need to return everything here, but that means we'll need some other
   875  	// place to mutate the response body...where?
   876  	templateString, templateStringErr := _escFSString(false, "/resources/provision/apigateway/outputmapping_json.vtl")
   877  	// Ignore any error when running in AWS, since that version of the binary won't
   878  	// have the embedded asset. This ideally would be done only when we're exporting
   879  	// the Method, but that would involve changing caller behavior since
   880  	// callers currently expect the method.Integration.Responses to be populated
   881  	// when this constructor returns.
   882  	if templateStringErr != nil {
   883  		templateString = _escFSMustString(false, "/resources/awsbinary/README.md")
   884  	}
   885  
   886  	// TODO - tell the caller that we don't need the list of all HTTP status
   887  	// codes anymore since we've moved everything to overrides in the VTL mapping.
   888  
   889  	// Populate Integration.Responses and the method Parameters
   890  	for _, i := range possibleHTTPStatusCodeResponses {
   891  		statusText := http.StatusText(i)
   892  		if statusText == "" {
   893  			return nil, fmt.Errorf("invalid HTTP status code %d provided for method: %s",
   894  				i,
   895  				httpMethod)
   896  		}
   897  
   898  		// The integration responses are keyed from supported error codes...
   899  		if defaultHTTPStatusCode == i {
   900  			// Since we pushed this into the VTL mapping, we don't need to create explicit RegExp based
   901  			// mappings for all of the user response codes. It will just work.
   902  			// Ref: https://docs.aws.amazon.com/apigateway/latest/developerguide/handle-errors-in-lambda-integration.html
   903  			method.Integration.Responses[i] = &IntegrationResponse{
   904  				Parameters: make(map[string]interface{}),
   905  				Templates: map[string]string{
   906  					"application/json": templateString,
   907  					"text/*":           "",
   908  				},
   909  				SelectionPattern: "",
   910  			}
   911  		}
   912  
   913  		// Then the Method.Responses
   914  		method.Responses[i] = &Response{
   915  			Parameters: make(map[string]bool),
   916  			Models:     make(map[string]*Model),
   917  		}
   918  	}
   919  	resource.Methods[keyname] = method
   920  	return method, nil
   921  }
   922  
   923  // NewAuthorizedMethod associates the httpMethod name and authorizationID with
   924  // the given Resource. The authorizerID param is a cloudformation.Strinable
   925  // satisfying value
   926  func (resource *Resource) NewAuthorizedMethod(httpMethod string,
   927  	authorizerID gocf.Stringable,
   928  	defaultHTTPStatusCode int,
   929  	possibleHTTPStatusCodeResponses ...int) (*Method, error) {
   930  	if authorizerID == nil {
   931  		return nil, fmt.Errorf("authorizerID must not be `nil` for Authorized Method")
   932  	}
   933  	method, methodErr := resource.NewMethod(httpMethod,
   934  		defaultHTTPStatusCode,
   935  		possibleHTTPStatusCodeResponses...)
   936  	if methodErr == nil {
   937  		method.authorizationID = authorizerID
   938  	}
   939  	return method, methodErr
   940  }