github.com/mweagle/Sparta@v1.15.0/apigatewayv2.go (about) 1 package sparta 2 3 import ( 4 "fmt" 5 6 "github.com/aws/aws-sdk-go/aws/session" 7 gocf "github.com/mweagle/go-cloudformation" 8 "github.com/pkg/errors" 9 "github.com/sirupsen/logrus" 10 ) 11 12 // Ref: https://github.com/aws-samples/simple-websockets-chat-app 13 14 // APIV2RouteSelectionExpression represents a route selection 15 type APIV2RouteSelectionExpression string 16 17 // APIV2Protocol is the type of API V2 protocols 18 type APIV2Protocol string 19 20 const ( 21 // Websocket represents the only supported V2 protocol 22 Websocket APIV2Protocol = "WEBSOCKET" 23 ) 24 25 // APIV2 contains the information necessary for the routes in here. 26 // Please tell me they can use the same routes... 27 // They cannot 28 type APIV2 struct { 29 protocol APIV2Protocol 30 name string 31 routeSelectionExpression string 32 stage *APIV2Stage 33 APIKeySelectionExpression string 34 Description string 35 DisableSchemaValidation bool 36 Tags map[string]interface{} 37 Version string 38 // Routes mapping selection expression to Route handler 39 routes map[APIV2RouteSelectionExpression]*APIV2Route 40 } 41 42 // APIV2GatewayDecorator is the compound decorator that handles both 43 // the DDB table creation and the lambda decorator...winning. 44 type APIV2GatewayDecorator struct { 45 envTableKeyName string 46 propertyName string 47 readCapacity int64 48 writeCapacity int64 49 } 50 51 func (apigd *APIV2GatewayDecorator) logicalResourceName() string { 52 return CloudFormationResourceName("WSSConnectionTable", 53 "WSSConnectionTable") 54 } 55 56 // DecorateService handles inserting the DDB Table 57 func (apigd *APIV2GatewayDecorator) DecorateService(context map[string]interface{}, 58 serviceName string, 59 template *gocf.Template, 60 S3Bucket string, 61 S3Key string, 62 buildID string, 63 awsSession *session.Session, 64 noop bool, 65 logger *logrus.Logger) error { 66 67 // Create the table... 68 dynamoDBResourceName := apigd.logicalResourceName() 69 dynamoDBResource := &gocf.DynamoDBTable{ 70 AttributeDefinitions: &gocf.DynamoDBTableAttributeDefinitionList{ 71 gocf.DynamoDBTableAttributeDefinition{ 72 AttributeName: gocf.String(apigd.propertyName), 73 AttributeType: gocf.String("S"), 74 }, 75 }, 76 KeySchema: &gocf.DynamoDBTableKeySchemaList{ 77 gocf.DynamoDBTableKeySchema{ 78 AttributeName: gocf.String(apigd.propertyName), 79 KeyType: gocf.String("HASH"), 80 }, 81 }, 82 SSESpecification: &gocf.DynamoDBTableSSESpecification{ 83 SSEEnabled: gocf.Bool(true), 84 }, 85 ProvisionedThroughput: &gocf.DynamoDBTableProvisionedThroughput{ 86 ReadCapacityUnits: gocf.Integer(apigd.readCapacity), 87 WriteCapacityUnits: gocf.Integer(apigd.writeCapacity), 88 }, 89 } 90 template.AddResource(dynamoDBResourceName, dynamoDBResource) 91 return nil 92 } 93 94 // AnnotateLambdas handles hooking up the lambda perms 95 func (apigd *APIV2GatewayDecorator) AnnotateLambdas(lambdaFns []*LambdaAWSInfo) error { 96 97 var ddbPermissions = []IAMRolePrivilege{ 98 { 99 Actions: []string{"dynamodb:GetItem", 100 "dynamodb:DeleteItem", 101 "dynamodb:PutItem", 102 "dynamodb:Scan", 103 "dynamodb:Query", 104 "dynamodb:UpdateItem", 105 "dynamodb:BatchWriteItem", 106 "dynamodb:BatchGetItem"}, 107 Resource: gocf.Join("", 108 gocf.String("arn:"), 109 gocf.Ref("AWS::Partition"), 110 gocf.String(":dynamodb:"), 111 gocf.Ref("AWS::Region"), 112 gocf.String(":"), 113 gocf.Ref("AWS::AccountId"), 114 gocf.String(":table/"), 115 gocf.Ref(apigd.logicalResourceName())), 116 }, 117 { 118 Actions: []string{"dynamodb:GetItem", 119 "dynamodb:DeleteItem", 120 "dynamodb:PutItem", 121 "dynamodb:Scan", 122 "dynamodb:Query", 123 "dynamodb:UpdateItem", 124 "dynamodb:BatchWriteItem", 125 "dynamodb:BatchGetItem"}, 126 Resource: gocf.Join("", 127 gocf.String("arn:"), 128 gocf.Ref("AWS::Partition"), 129 gocf.String(":dynamodb:"), 130 gocf.Ref("AWS::Region"), 131 gocf.String(":"), 132 gocf.Ref("AWS::AccountId"), 133 gocf.String(":table/"), 134 gocf.Ref(apigd.logicalResourceName()), 135 gocf.String("/index/*")), 136 }, 137 } 138 139 for _, eachLambda := range lambdaFns { 140 // Add the permission 141 eachLambda.RoleDefinition.Privileges = append(eachLambda.RoleDefinition.Privileges, 142 ddbPermissions...) 143 144 // Add the env 145 env := eachLambda.Options.Environment 146 if env == nil { 147 env = make(map[string]*gocf.StringExpr) 148 } 149 env[apigd.envTableKeyName] = gocf.Ref(apigd.logicalResourceName()).String() 150 eachLambda.Options.Environment = env 151 } 152 return nil 153 } 154 155 // NewConnectionTableDecorator returns a *APIV2GatewayDecorator that handles 156 // creating the DynamoDDB table and hooking up all the lambda permissions 157 func (apiv2 *APIV2) NewConnectionTableDecorator(envTableNameKey string, 158 propertyName string, 159 readCapacity int64, 160 writeCapacity int64) (*APIV2GatewayDecorator, error) { 161 162 return &APIV2GatewayDecorator{ 163 envTableKeyName: envTableNameKey, 164 propertyName: propertyName, 165 readCapacity: readCapacity, 166 writeCapacity: writeCapacity, 167 }, nil 168 } 169 170 // NewAPIV2Route returns a new Route 171 func (apiv2 *APIV2) NewAPIV2Route(routeKey APIV2RouteSelectionExpression, 172 lambdaFn *LambdaAWSInfo) (*APIV2Route, error) { 173 174 _, exists := apiv2.routes[routeKey] 175 if exists { 176 return nil, errors.Errorf("APIV2 Route for expression `%s` already exists", 177 routeKey) 178 } 179 route := &APIV2Route{ 180 routeKey: routeKey, 181 lambdaFn: lambdaFn, 182 Integration: &APIV2Integration{ 183 IntegrationType: "AWS_PROXY", 184 }, 185 } 186 apiv2.routes[routeKey] = route 187 return route, nil 188 } 189 190 // LogicalResourceName returns the logical resoource name of this API V2 Gateway 191 // instance 192 func (apiv2 *APIV2) LogicalResourceName() string { 193 return CloudFormationResourceName("APIGateway", 194 fmt.Sprintf("v2%s", apiv2.name)) 195 } 196 197 // Describe satisfies the API interface 198 func (apiv2 *APIV2) Describe(targetNodeName string) (*DescriptionInfo, error) { 199 descInfo := &DescriptionInfo{ 200 Name: "APIGatewayV2", 201 Nodes: make([]*DescriptionTriplet, 0), 202 } 203 204 descInfo.Nodes = append(descInfo.Nodes, &DescriptionTriplet{ 205 SourceNodeName: nodeNameAPIGateway, 206 DisplayInfo: &DescriptionDisplayInfo{ 207 SourceNodeColor: nodeColorAPIGateway, 208 SourceIcon: &DescriptionIcon{ 209 Category: "Mobile", 210 Name: "Amazon-API-Gateway_light-bg@4x.png", 211 }, 212 }, 213 TargetNodeName: targetNodeName, 214 }) 215 216 for eachRouteExpr, eachRoute := range apiv2.routes { 217 opName := "" 218 if eachRoute.OperationName != "" { 219 opName = fmt.Sprintf(" - %s", eachRoute.OperationName) 220 } 221 var nodeName = fmt.Sprintf("%s%s", eachRouteExpr, opName) 222 223 descInfo.Nodes = append(descInfo.Nodes, 224 &DescriptionTriplet{ 225 SourceNodeName: nodeNameAPIGateway, 226 DisplayInfo: &DescriptionDisplayInfo{ 227 SourceNodeColor: nodeColorAPIGateway, 228 SourceIcon: &DescriptionIcon{ 229 Category: "_General", 230 Name: "Internet-alt1_light-bg@4x.png", 231 }, 232 }, 233 TargetNodeName: nodeName, 234 }, 235 &DescriptionTriplet{ 236 SourceNodeName: nodeName, 237 TargetNodeName: eachRoute.lambdaFn.lambdaFunctionName(), 238 }) 239 } 240 return descInfo, nil 241 } 242 243 // Marshal marshals the API V2 Gateway instance to the given template instance 244 func (apiv2 *APIV2) Marshal(serviceName string, 245 session *session.Session, 246 S3Bucket string, 247 S3Key string, 248 S3Version string, 249 roleNameMap map[string]*gocf.StringExpr, 250 template *gocf.Template, 251 noop bool, 252 logger *logrus.Logger) error { 253 254 apiV2Entry := &gocf.APIGatewayV2API{ 255 APIKeySelectionExpression: marshalString(apiv2.APIKeySelectionExpression), 256 Description: marshalString(apiv2.Description), 257 DisableSchemaValidation: marshalBool(apiv2.DisableSchemaValidation), 258 Name: marshalString(apiv2.name), 259 ProtocolType: marshalString(string(Websocket)), 260 RouteSelectionExpression: marshalString(apiv2.routeSelectionExpression), 261 Version: marshalString(apiv2.Version), 262 } 263 // Add it 264 apiV2Resource := template.AddResource(apiv2.LogicalResourceName(), apiV2Entry) 265 apiV2Resource.DependsOn = []string{} 266 allRouteResources := []string{} 267 268 // Alright, setup the route 269 for eachExpression, eachRoute := range apiv2.routes { 270 routeResourceName := CloudFormationResourceName("Route", string(eachExpression)) 271 allRouteResources = append(allRouteResources, routeResourceName) 272 273 routeIntegrationResourceName := CloudFormationResourceName("RouteIntg", string(eachExpression)) 274 routeEntry := &gocf.APIGatewayV2Route{ 275 APIID: gocf.Ref(apiv2.LogicalResourceName()).String(), 276 APIKeyRequired: marshalBool(eachRoute.APIKeyRequired), 277 AuthorizerID: marshalStringExpr(eachRoute.AuthorizerID), 278 AuthorizationScopes: marshalStringList(eachRoute.AuthorizationScopes), 279 AuthorizationType: marshalString(eachRoute.AuthorizationType), 280 ModelSelectionExpression: marshalString(eachRoute.ModelSelectionExpression), 281 OperationName: marshalString(eachRoute.OperationName), 282 RequestModels: marshalInterface(eachRoute.RequestModels), 283 RequestParameters: marshalInterface(eachRoute.RequestParameters), 284 RouteKey: marshalString(string(eachRoute.routeKey)), 285 RouteResponseSelectionExpression: marshalString(eachRoute.RouteResponseSelectionExpression), 286 Target: gocf.Join("/", 287 gocf.String("integrations"), 288 gocf.Ref(routeIntegrationResourceName)), 289 } 290 291 // Add the route resource 292 template.AddResource(routeResourceName, routeEntry) 293 //apiV2Resource.DependsOn = append(apiV2Resource.DependsOn, routeResourceName) 294 295 // Add the integration 296 routeIntegration := &gocf.APIGatewayV2Integration{ 297 APIID: gocf.Ref(apiv2.LogicalResourceName()).String(), 298 ConnectionType: marshalString(eachRoute.Integration.ConnectionType), 299 ContentHandlingStrategy: marshalString(eachRoute.Integration.ContentHandlingStrategy), 300 CredentialsArn: marshalStringExpr(eachRoute.Integration.CredentialsArn), 301 Description: marshalString(eachRoute.Integration.Description), 302 IntegrationMethod: marshalString(eachRoute.Integration.IntegrationMethod), 303 IntegrationType: marshalString(eachRoute.Integration.IntegrationType), 304 IntegrationURI: gocf.Join("", 305 gocf.String("arn:aws:apigateway:"), 306 gocf.Ref("AWS::Region"), 307 gocf.String(":lambda:path/2015-03-31/functions/"), 308 gocf.GetAtt(eachRoute.lambdaFn.LogicalResourceName(), "Arn"), 309 gocf.String("/invocations")), 310 PassthroughBehavior: marshalString(eachRoute.Integration.PassthroughBehavior), 311 // TODO - auto create this... 312 RequestParameters: marshalInterface(eachRoute.Integration.RequestParameters), 313 RequestTemplates: marshalInterface(eachRoute.Integration.RequestTemplates), 314 TemplateSelectionExpression: marshalString(eachRoute.Integration.TemplateSelectionExpression), 315 TimeoutInMillis: marshalInt(eachRoute.Integration.TimeoutInMillis), 316 } 317 template.AddResource(routeIntegrationResourceName, routeIntegration) 318 319 // Add the lambda permission 320 apiGatewayPermissionResourceName := CloudFormationResourceName("APIV2GatewayLambdaPerm", 321 string(eachExpression)) 322 lambdaInvokePermission := &gocf.LambdaPermission{ 323 Action: gocf.String("lambda:InvokeFunction"), 324 FunctionName: gocf.GetAtt(eachRoute.lambdaFn.LogicalResourceName(), "Arn"), 325 Principal: gocf.String(APIGatewayPrincipal), 326 } 327 template.AddResource(apiGatewayPermissionResourceName, lambdaInvokePermission) 328 } 329 330 // Add the Stage and Deploy... 331 stageResourceName := CloudFormationResourceName("APIV2GatewayStage", "APIV2GatewayStage") 332 // Use an unstable ID s.t. we can actually create a new deployment event. 333 deploymentResName := CloudFormationResourceName("APIV2GatewayDeployment") 334 335 // Unstable name to trigger a deployment 336 newDeployment := &gocf.APIGatewayV2Deployment{ 337 APIID: gocf.Ref(apiv2.LogicalResourceName()).String(), 338 Description: marshalString(apiv2.stage.Description), 339 } 340 // Use an unstable ID s.t. we can actually create a new deployment event. Not sure how this 341 // is going to work with deletes... 342 deployment := template.AddResource(deploymentResName, newDeployment) 343 deployment.DeletionPolicy = "Retain" 344 deployment.DependsOn = append(deployment.DependsOn, allRouteResources...) 345 346 // Add the stage... 347 stageResource := &gocf.APIGatewayV2Stage{ 348 APIID: gocf.Ref(apiv2.LogicalResourceName()).String(), 349 DeploymentID: gocf.Ref(deploymentResName).String(), 350 StageName: marshalString(apiv2.stage.name), 351 AccessLogSettings: apiv2.stage.AccessLogSettings, 352 ClientCertificateID: marshalStringExpr(apiv2.stage.ClientCertificateID), 353 DefaultRouteSettings: apiv2.stage.DefaultRouteSettings, 354 Description: marshalString(apiv2.stage.Description), 355 RouteSettings: marshalInterface(apiv2.stage.RouteSettings), 356 StageVariables: marshalInterface(apiv2.stage.StageVariables), 357 //Tags: marshalInterface(apiv2.stage.Tags), 358 } 359 template.AddResource(stageResourceName, stageResource) 360 361 // Outputs... 362 template.Outputs[OutputAPIGatewayURL] = &gocf.Output{ 363 Description: "API Gateway Websocket URL", 364 Value: gocf.Join("", 365 gocf.String("wss://"), 366 gocf.Ref(apiv2.LogicalResourceName()), 367 gocf.String(".execute-api."), 368 gocf.Ref("AWS::Region"), 369 gocf.String(".amazonaws.com/"), 370 gocf.String(apiv2.stage.name)), 371 } 372 return nil 373 } 374 375 // NewAPIV2 returns a new API V2 Gateway instance 376 func NewAPIV2(protocol APIV2Protocol, 377 name string, 378 routeSelectionExpression string, 379 stage *APIV2Stage) (*APIV2, error) { 380 return &APIV2{ 381 protocol: protocol, 382 name: name, 383 routeSelectionExpression: routeSelectionExpression, 384 stage: stage, 385 routes: make(map[APIV2RouteSelectionExpression]*APIV2Route), 386 Tags: make(map[string]interface{}), 387 }, nil 388 } 389 390 // APIV2Route represents a V2 route 391 type APIV2Route struct { 392 routeKey APIV2RouteSelectionExpression 393 APIKeyRequired bool 394 AuthorizationScopes []string 395 AuthorizationType string 396 AuthorizerID gocf.Stringable 397 ModelSelectionExpression string 398 OperationName string 399 RequestModels interface{} 400 RequestParameters interface{} 401 RouteResponseSelectionExpression string 402 Integration *APIV2Integration 403 lambdaFn *LambdaAWSInfo 404 } 405 406 // APIV2Stage represents the deployment stage 407 type APIV2Stage struct { 408 AccessLogSettings *gocf.APIGatewayV2StageAccessLogSettings 409 ClientCertificateID gocf.Stringable 410 DefaultRouteSettings *gocf.APIGatewayV2StageRouteSettings 411 Description string 412 RouteSettings interface{} 413 name string 414 StageVariables interface{} 415 Tags interface{} 416 } 417 418 // NewAPIV2Stage returns a new APIV2Stage entry 419 func NewAPIV2Stage(stageName string) (*APIV2Stage, error) { 420 return &APIV2Stage{ 421 name: stageName, 422 }, nil 423 } 424 425 // APIV2Integration is the integration type for an APIV2Route 426 // entry 427 type APIV2Integration struct { 428 //ApiID string 429 ConnectionType string 430 ContentHandlingStrategy string 431 CredentialsArn gocf.Stringable 432 Description string 433 IntegrationMethod string 434 IntegrationType string 435 //IntegrationUri string 436 PassthroughBehavior string 437 RequestParameters interface{} 438 RequestTemplates interface{} 439 TemplateSelectionExpression string 440 TimeoutInMillis int64 441 }