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 }