github.com/mweagle/Sparta@v1.15.0/sparta.go (about) 1 package sparta 2 3 import ( 4 "context" 5 "crypto/sha1" 6 "encoding/hex" 7 "encoding/json" 8 "fmt" 9 "math/rand" 10 "os" 11 "reflect" 12 "regexp" 13 "runtime" 14 "strings" 15 "time" 16 17 spartaCF "github.com/mweagle/Sparta/aws/cloudformation" 18 spartaIAM "github.com/mweagle/Sparta/aws/iam" 19 gocc "github.com/mweagle/go-cloudcondenser" 20 gocf "github.com/mweagle/go-cloudformation" 21 "github.com/pkg/errors" 22 "github.com/sirupsen/logrus" 23 ) 24 25 type cloudFormationLambdaCustomResource struct { 26 gocf.CloudFormationCustomResource 27 ServiceToken *gocf.StringExpr 28 UserProperties map[string]interface{} `json:",omitempty"` 29 } 30 31 func customResourceProvider(resourceType string) gocf.ResourceProperties { 32 switch resourceType { 33 case cloudFormationLambda: 34 { 35 return &cloudFormationLambdaCustomResource{} 36 } 37 default: 38 return nil 39 } 40 } 41 42 func init() { 43 gocf.RegisterCustomResourceProvider(customResourceProvider) 44 rand.Seed(time.Now().Unix()) 45 } 46 47 func noopMessage(operationName string) string { 48 return fmt.Sprintf("Skipping %s due to -n/-noop flag", 49 operationName) 50 } 51 52 /******************************************************************************/ 53 // Global options 54 type optionsGlobalStruct struct { 55 ServiceName string `validate:"required"` 56 ServiceDescription string `validate:"-"` 57 Noop bool `validate:"-"` 58 LogLevel string `validate:"eq=panic|eq=fatal|eq=error|eq=warn|eq=info|eq=debug"` 59 LogFormat string `validate:"eq=txt|eq=text|eq=json"` 60 TimeStamps bool `validate:"-"` 61 Logger *logrus.Logger `validate:"-"` 62 Command string `validate:"-"` 63 BuildTags string `validate:"-"` 64 LinkerFlags string `validate:"-"` // no requirements 65 DisableColors bool `validate:"-"` 66 } 67 68 // OptionsGlobal stores the global command line options 69 var OptionsGlobal optionsGlobalStruct 70 71 //////////////////////////////////////////////////////////////////////////////// 72 // Variables 73 //////////////////////////////////////////////////////////////////////////////// 74 75 // Represents the CloudFormation Arn of this stack, referenced 76 // in CommonIAMStatements 77 var cloudFormationThisStackArn = []gocf.Stringable{gocf.String("arn:aws:cloudformation:"), 78 gocf.Ref("AWS::Region").String(), 79 gocf.String(":"), 80 gocf.Ref("AWS::AccountId").String(), 81 gocf.String(":stack/"), 82 gocf.Ref("AWS::StackName").String(), 83 gocf.String("/*")} 84 85 // CommonIAMStatements defines common IAM::Role Policy Statement values for different AWS 86 // service types. See http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#genref-aws-service-namespaces 87 // for names. 88 // http://docs.aws.amazon.com/lambda/latest/dg/monitoring-functions.html 89 // for more information. 90 var CommonIAMStatements = struct { 91 Core []spartaIAM.PolicyStatement 92 VPC []spartaIAM.PolicyStatement 93 DynamoDB []spartaIAM.PolicyStatement 94 Kinesis []spartaIAM.PolicyStatement 95 SQS []spartaIAM.PolicyStatement 96 }{ 97 Core: []spartaIAM.PolicyStatement{ 98 { 99 Action: []string{"logs:CreateLogGroup", 100 "logs:CreateLogStream", 101 "logs:PutLogEvents"}, 102 Effect: "Allow", 103 Resource: gocf.Join("", 104 gocf.String("arn:aws:logs:"), 105 gocf.Ref("AWS::Region"), 106 gocf.String(":"), 107 gocf.Ref("AWS::AccountId"), 108 gocf.String(":*")), 109 }, 110 { 111 Effect: "Allow", 112 Action: []string{"cloudformation:DescribeStacks", 113 "cloudformation:DescribeStackResource"}, 114 Resource: gocf.Join("", cloudFormationThisStackArn...), 115 }, 116 // http://docs.aws.amazon.com/lambda/latest/dg/lambda-x-ray.html#enabling-x-ray 117 { 118 Effect: "Allow", 119 Action: []string{"xray:PutTraceSegments", 120 "xray:PutTelemetryRecords", 121 "cloudwatch:PutMetricData"}, 122 Resource: gocf.String("*"), 123 }, 124 }, 125 VPC: []spartaIAM.PolicyStatement{ 126 { 127 Action: []string{"ec2:CreateNetworkInterface", 128 "ec2:DescribeNetworkInterfaces", 129 "ec2:DeleteNetworkInterface"}, 130 Effect: "Allow", 131 Resource: wildcardArn, 132 }, 133 }, 134 DynamoDB: []spartaIAM.PolicyStatement{ 135 { 136 Effect: "Allow", 137 Action: []string{"dynamodb:DescribeStream", 138 "dynamodb:GetRecords", 139 "dynamodb:GetShardIterator", 140 "dynamodb:ListStreams", 141 }, 142 }, 143 }, 144 Kinesis: []spartaIAM.PolicyStatement{ 145 { 146 Effect: "Allow", 147 Action: []string{"kinesis:GetRecords", 148 "kinesis:GetShardIterator", 149 "kinesis:DescribeStream", 150 "kinesis:ListStreams", 151 }, 152 }, 153 }, 154 // https://docs.aws.amazon.com/lambda/latest/dg/with-sqs-create-execution-role.html 155 SQS: []spartaIAM.PolicyStatement{ 156 { 157 Effect: "Allow", 158 Action: []string{"SQS:GetQueueAttributes", 159 "SQS:ChangeMessageVisibility", 160 "SQS:DeleteMessage", 161 "SQS:ReceiveMessage", 162 }, 163 }, 164 }, 165 } 166 167 // RE for sanitizing names 168 var reSanitize = regexp.MustCompile(`\W+`) 169 170 // Wildcard ARN for any AWS resource 171 var wildcardArn = gocf.String("*") 172 173 // AssumePolicyDocument defines common a IAM::Role PolicyDocument 174 // used as part of IAM::Role resource definitions 175 var AssumePolicyDocument = ArbitraryJSONObject{ 176 "Version": "2012-10-17", 177 "Statement": []ArbitraryJSONObject{ 178 { 179 "Effect": "Allow", 180 "Principal": ArbitraryJSONObject{ 181 "Service": []string{LambdaPrincipal, 182 EC2Principal, 183 APIGatewayPrincipal}, 184 }, 185 "Action": []string{"sts:AssumeRole"}, 186 }, 187 }, 188 } 189 190 //////////////////////////////////////////////////////////////////////////////// 191 // Types 192 //////////////////////////////////////////////////////////////////////////////// 193 194 // ArbitraryJSONObject represents an untyped key-value object. CloudFormation resource representations 195 // are aggregated as []ArbitraryJSONObject before being marsharled to JSON 196 // for API operations. 197 type ArbitraryJSONObject map[string]interface{} 198 199 // LambdaContext defines the AWS Lambda Context object provided by the AWS Lambda runtime. 200 // See http://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html 201 // for more information on field values. Note that the golang version doesn't functions 202 // defined on the Context object. 203 type LambdaContext struct { 204 FunctionName string `json:"functionName"` 205 FunctionVersion string `json:"functionVersion"` 206 InvokedFunctionARN string `json:"invokedFunctionArn"` 207 MemoryLimitInMB string `json:"memoryLimitInMB"` 208 AWSRequestID string `json:"awsRequestId"` 209 LogGroupName string `json:"logGroupName"` 210 LogStreamName string `json:"logStreamName"` 211 } 212 213 // LambdaFunctionOptions defines additional AWS Lambda execution params. See the 214 // AWS Lambda FunctionConfiguration (http://docs.aws.amazon.com/lambda/latest/dg/API_FunctionConfiguration.html) 215 // docs for more information. Note that the "Runtime" field will be automatically set 216 // to "nodejs4.3" (at least until golang is officially supported). See 217 // http://docs.aws.amazon.com/lambda/latest/dg/programming-model.html 218 type LambdaFunctionOptions struct { 219 // Additional function description 220 Description string 221 // Memory limit 222 MemorySize int64 223 // Timeout (seconds) 224 Timeout int64 225 // VPC Settings 226 VpcConfig *gocf.LambdaFunctionVPCConfig 227 // Environment Variables 228 Environment map[string]*gocf.StringExpr 229 // KMS Key Arn used to encrypt environment variables 230 KmsKeyArn string 231 // The maximum of concurrent executions you want reserved for the function 232 ReservedConcurrentExecutions int64 233 // DeadLetterConfigArn is how Lambda handles events that it can't process.If 234 // you don't specify a Dead Letter Queue (DLQ) configuration, Lambda 235 // discards events after the maximum number of retries. For more information, 236 // see Dead Letter Queues in the AWS Lambda Developer Guide. 237 DeadLetterConfigArn gocf.Stringable 238 // Tags to associate with the Lambda function 239 Tags map[string]string 240 // Tracing options for XRay 241 TracingConfig *gocf.LambdaFunctionTracingConfig 242 // Additional params 243 SpartaOptions *SpartaOptions 244 } 245 246 func defaultLambdaFunctionOptions() *LambdaFunctionOptions { 247 return &LambdaFunctionOptions{Description: "", 248 MemorySize: 128, 249 Timeout: 3, 250 VpcConfig: nil, 251 Environment: make(map[string]*gocf.StringExpr), 252 KmsKeyArn: "", 253 ReservedConcurrentExecutions: 0, 254 SpartaOptions: nil, 255 } 256 } 257 258 // SpartaOptions allow the passing in of additional options during the creation of a Lambda Function 259 type SpartaOptions struct { 260 // User supplied function name to use for 261 // http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html#cfn-lambda-function-functionname 262 // value. If this is not supplied, a reflection-based 263 // name will be automatically used. 264 Name string 265 } 266 267 // WorkflowHooks is a structure that allows callers to customize the Sparta provisioning 268 // pipeline to add contents the Lambda archive or perform other workflow operations. 269 // TODO: remove single-valued fields 270 type WorkflowHooks struct { 271 // Initial hook context. May be empty 272 Context map[string]interface{} 273 // PreBuild is called before the current Sparta-binary is compiled 274 PreBuild WorkflowHook 275 // PreBuilds are called before the current Sparta-binary is compiled 276 PreBuilds []WorkflowHookHandler 277 // PostBuild is called after the current Sparta-binary is compiled 278 PostBuild WorkflowHook 279 // PostBuilds are called after the current Sparta-binary is compiled 280 PostBuilds []WorkflowHookHandler 281 // ArchiveHook is called after Sparta has populated the ZIP archive containing the 282 // AWS Lambda code package and before the ZIP writer is closed. Define this hook 283 // to add additional resource files to your Lambda package 284 Archive ArchiveHook 285 // ArchiveHook is called after Sparta has populated the ZIP archive containing the 286 // AWS Lambda code package and before the ZIP writer is closed. Define this hook 287 // to add additional resource files to your Lambda package 288 Archives []ArchiveHookHandler 289 // PreMarshall is called before Sparta marshalls the application contents to a CloudFormation template 290 PreMarshall WorkflowHook 291 // PreMarshalls are called before Sparta marshalls the application contents into a CloudFormation 292 // template 293 PreMarshalls []WorkflowHookHandler 294 // ServiceDecorator is called before Sparta marshalls the CloudFormation template 295 ServiceDecorator ServiceDecoratorHook 296 // ServiceDecorators are called before Sparta marshalls the CloudFormation template 297 ServiceDecorators []ServiceDecoratorHookHandler 298 // PostMarshall is called after Sparta marshalls the application contents to a CloudFormation template 299 PostMarshall WorkflowHook 300 // PostMarshalls are called after Sparta marshalls the application contents to a CloudFormation 301 // template 302 PostMarshalls []WorkflowHookHandler 303 304 // Validators are hooks that are called when all marshalling 305 // is complete. Each hook receives a complete read-only 306 // copy of the materialized template. 307 Validators []ServiceValidationHookHandler 308 309 // Rollback is called if there is an error performing the requested operation 310 Rollback RollbackHook 311 // Rollbacks are called if there is an error performing the requested operation 312 Rollbacks []RollbackHookHandler 313 } 314 315 //////////////////////////////////////////////////////////////////////////////// 316 // START - IAMRolePrivilege 317 // 318 319 // IAMRolePrivilege struct stores data necessary to create an IAM Policy Document 320 // as part of the inline IAM::Role resource definition. See 321 // http://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html 322 // for more information 323 // Deprecated: Prefer github.com/aws/iam/PolicyStatement instead. 324 type IAMRolePrivilege struct { 325 // What actions you will allow. 326 // Each AWS service has its own set of actions. 327 // For example, you might allow a user to use the Amazon S3 ListBucket action, 328 // which returns information about the items in a bucket. 329 // Any actions that you don't explicitly allow are denied. 330 Actions []string 331 // Which resources you allow the action on. For example, what specific Amazon 332 // S3 buckets will you allow the user to perform the ListBucket action on? 333 // Users cannot access any resources that you have not explicitly granted 334 // permissions to. 335 Resource interface{} `json:",omitempty"` 336 // Service that requires the action 337 Principal interface{} `json:",omitempty"` 338 // Optional condition for the privilege 339 Condition interface{} `json:",omitempty"` 340 } 341 342 func (rolePrivilege *IAMRolePrivilege) resourceExpr() *gocf.StringExpr { 343 switch typedPrivilege := rolePrivilege.Resource.(type) { 344 case string: 345 return gocf.String(typedPrivilege) 346 case gocf.RefFunc: 347 return typedPrivilege.String() 348 default: 349 return typedPrivilege.(*gocf.StringExpr) 350 } 351 } 352 353 // IAMRoleDefinition stores a slice of IAMRolePrivilege values 354 // to "Allow" for the given IAM::Role. 355 // Note that the CommonIAMStatements will be automatically included and do 356 // not need to be multiply specified. 357 type IAMRoleDefinition struct { 358 // Slice of IAMRolePrivilege entries 359 Privileges []IAMRolePrivilege 360 // Cached logical resource name 361 cachedLogicalName string 362 } 363 364 func (roleDefinition *IAMRoleDefinition) toResource(eventSourceMappings []*EventSourceMapping, 365 options *LambdaFunctionOptions, 366 logger *logrus.Logger) gocf.IAMRole { 367 368 statements := CommonIAMStatements.Core 369 for _, eachPrivilege := range roleDefinition.Privileges { 370 policyStatement := spartaIAM.PolicyStatement{ 371 Effect: "Allow", 372 Action: eachPrivilege.Actions, 373 Resource: eachPrivilege.resourceExpr(), 374 } 375 statements = append(statements, policyStatement) 376 } 377 378 // Add VPC permissions iff needed 379 if options != nil && options.VpcConfig != nil { 380 statements = append(statements, CommonIAMStatements.VPC...) 381 } 382 // In the past Sparta used to attach EventSourceMapping policies here. 383 // However, moving everything to dynamic references means that we can't 384 // fully populate the PolicyDocument statement slice until all of 385 // the dynamically provisioned resources are defined. So that logic has 386 // been moved to annotateMaterializedTemplate and annotateEventSourceMappings 387 // which is run as the final step right before the template is marshaled 388 // for creation. 389 390 // http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html 391 iamPolicies := gocf.IAMRolePolicyList{} 392 iamPolicies = append(iamPolicies, gocf.IAMRolePolicy{ 393 PolicyDocument: ArbitraryJSONObject{ 394 "Version": "2012-10-17", 395 "Statement": statements, 396 }, 397 PolicyName: gocf.String("LambdaPolicy"), 398 }) 399 return gocf.IAMRole{ 400 AssumeRolePolicyDocument: AssumePolicyDocument, 401 Policies: &iamPolicies, 402 } 403 } 404 405 // Returns the stable logical name for this IAMRoleDefinition, which depends on the serviceName 406 // and owning targetLambdaFnName. This potentially creates semantically equivalent IAM::Role entries 407 // from the same struct pointer, so: 408 // TODO: Create a canonical IAMRoleDefinition serialization that can be used as the digest source 409 func (roleDefinition *IAMRoleDefinition) logicalName(serviceName string, targetLambdaFnName string) string { 410 if roleDefinition.cachedLogicalName == "" { 411 roleDefinition.cachedLogicalName = CloudFormationResourceName("IAMRole", serviceName, targetLambdaFnName) 412 } 413 return roleDefinition.cachedLogicalName 414 } 415 416 // 417 // END - IAMRolePrivilege 418 //////////////////////////////////////////////////////////////////////////////// 419 420 //////////////////////////////////////////////////////////////////////////////// 421 // START - EventSourceMapping 422 423 // EventSourceMapping specifies data necessary for pull-based configuration. The fields 424 // directly correspond to the golang AWS SDK's CreateEventSourceMappingInput 425 // (http://docs.aws.amazon.com/sdk-for-go/api/service/lambda.html#type-CreateEventSourceMappingInput) 426 type EventSourceMapping struct { 427 StartingPosition string 428 EventSourceArn interface{} 429 Disabled bool 430 BatchSize int64 431 BisectBatchOnFunctionError bool 432 DestinationConfig *gocf.LambdaEventSourceMappingDestinationConfig 433 MaximumBatchingWindowInSeconds int64 434 MaximumRecordAgeInSeconds int64 435 MaximumRetryAttempts int64 436 ParallelizationFactor int64 437 } 438 439 func (mapping *EventSourceMapping) export(serviceName string, 440 targetLambdaName string, 441 targetLambdaArn *gocf.StringExpr, 442 S3Bucket string, 443 S3Key string, 444 template *gocf.Template, 445 logger *logrus.Logger) error { 446 447 dynamicArn := spartaCF.DynamicValueToStringExpr(mapping.EventSourceArn) 448 eventSourceMappingResource := gocf.LambdaEventSourceMapping{ 449 StartingPosition: marshalString(mapping.StartingPosition), 450 EventSourceArn: dynamicArn.String(), 451 FunctionName: targetLambdaArn, 452 BatchSize: gocf.Integer(mapping.BatchSize), 453 Enabled: gocf.Bool(!mapping.Disabled), 454 BisectBatchOnFunctionError: gocf.Bool(mapping.BisectBatchOnFunctionError), 455 DestinationConfig: mapping.DestinationConfig, 456 MaximumBatchingWindowInSeconds: marshalInt(mapping.MaximumBatchingWindowInSeconds), 457 MaximumRecordAgeInSeconds: marshalInt(mapping.MaximumRecordAgeInSeconds), 458 MaximumRetryAttempts: marshalInt(mapping.MaximumRetryAttempts), 459 ParallelizationFactor: marshalInt(mapping.ParallelizationFactor), 460 } 461 462 // Unique components for the hash for the EventSource mapping 463 // resource name 464 hashParts := []string{ 465 targetLambdaName, 466 dynamicArn.String().Literal, 467 targetLambdaArn.Literal, 468 fmt.Sprintf("%d", mapping.BatchSize), 469 mapping.StartingPosition, 470 } 471 hash := sha1.New() 472 for _, eachHashPart := range hashParts { 473 _, writeErr := hash.Write([]byte(eachHashPart)) 474 if writeErr != nil { 475 return errors.Wrapf(writeErr, 476 "Failed to update EventSourceMapping name: %s", eachHashPart) 477 } 478 } 479 resourceName := fmt.Sprintf("LambdaES%s", hex.EncodeToString(hash.Sum(nil))) 480 template.AddResource(resourceName, eventSourceMappingResource) 481 return nil 482 } 483 484 // 485 // END - EventSourceMapping 486 //////////////////////////////////////////////////////////////////////////////// 487 488 //////////////////////////////////////////////////////////////////////////////// 489 // START - customResourceInfo 490 491 // customResourceInfo wraps up information about any userDefined CloudFormation 492 // user-defined Resources 493 type customResourceInfo struct { 494 roleDefinition *IAMRoleDefinition 495 roleName string 496 handlerSymbol interface{} 497 userFunctionName string 498 options *LambdaFunctionOptions 499 properties map[string]interface{} 500 } 501 502 // Returns the stable CloudFormation resource logical name for this resource. For 503 // a CustomResource, this name corresponds to the AWS::CloudFormation::CustomResource 504 // invocation of the Lambda function, not the lambda function itself 505 func (resourceInfo *customResourceInfo) logicalName() string { 506 hash := sha1.New() 507 // The name has to be stable so that the ServiceToken value which is 508 // part the CustomResource invocation doesn't change during stack updates. CF 509 // will throw an error if the ServiceToken changes across updates. 510 source := fmt.Sprintf("%#v", resourceInfo.userFunctionName) 511 _, writeErr := hash.Write([]byte(source)) 512 if writeErr != nil { 513 fmt.Printf("TODO: failed to update hash. Error: %s", writeErr) 514 } 515 return CloudFormationResourceName(resourceInfo.userFunctionName, 516 hex.EncodeToString(hash.Sum(nil))) 517 } 518 519 func (resourceInfo *customResourceInfo) export(serviceName string, 520 targetLambda *gocf.StringExpr, 521 S3Bucket string, 522 S3Key string, 523 roleNameMap map[string]*gocf.StringExpr, 524 template *gocf.Template, 525 logger *logrus.Logger) error { 526 527 // Is this valid 528 invalidErr := ensureValidSignature(resourceInfo.userFunctionName, 529 resourceInfo.handlerSymbol) 530 if invalidErr != nil { 531 return invalidErr 532 } 533 534 // Figure out the role name 535 iamRoleArnName := resourceInfo.roleName 536 537 // If there is no user supplied role, that means that the associated 538 // IAMRoleDefinition name has been created and this resource needs to 539 // depend on that being created. 540 if iamRoleArnName == "" && resourceInfo.roleDefinition != nil { 541 iamRoleArnName = resourceInfo.roleDefinition.logicalName(serviceName, 542 resourceInfo.userFunctionName) 543 } 544 lambdaDescription := resourceInfo.options.Description 545 if lambdaDescription == "" { 546 lambdaDescription = fmt.Sprintf("%s CustomResource: %s", 547 serviceName, 548 resourceInfo.userFunctionName) 549 } 550 551 // Create the Lambda Function 552 lambdaFunctionName := awsLambdaFunctionName(resourceInfo.userFunctionName) 553 554 lambdaEnv, lambdaEnvErr := lambdaFunctionEnvironment(nil, 555 resourceInfo.userFunctionName, 556 nil, 557 logger) 558 if lambdaEnvErr != nil { 559 return errors.Wrapf(lambdaEnvErr, "Failed to create environment resource for custom info") 560 } 561 562 lambdaResource := gocf.LambdaFunction{ 563 Code: &gocf.LambdaFunctionCode{ 564 S3Bucket: gocf.String(S3Bucket), 565 S3Key: gocf.String(S3Key), 566 }, 567 FunctionName: lambdaFunctionName.String(), 568 Description: gocf.String(lambdaDescription), 569 Handler: gocf.String(SpartaBinaryName), 570 MemorySize: gocf.Integer(resourceInfo.options.MemorySize), 571 Role: roleNameMap[iamRoleArnName], 572 Runtime: gocf.String(GoLambdaVersion), 573 Timeout: gocf.Integer(resourceInfo.options.Timeout), 574 VPCConfig: resourceInfo.options.VpcConfig, 575 // DISPATCH INFORMATION 576 Environment: lambdaEnv, 577 } 578 579 lambdaFunctionCFName := CloudFormationResourceName("CustomResourceLambda", 580 resourceInfo.userFunctionName, 581 resourceInfo.logicalName()) 582 583 cfResource := template.AddResource(lambdaFunctionCFName, lambdaResource) 584 safeMetadataInsert(cfResource, "golangFunc", resourceInfo.userFunctionName) 585 586 // And create the CustomResource that actually invokes it... 587 newResource, newResourceError := newCloudFormationResource(cloudFormationLambda, logger) 588 if nil != newResourceError { 589 return newResourceError 590 } 591 customResource := newResource.(*cloudFormationLambdaCustomResource) 592 customResource.ServiceToken = gocf.GetAtt(lambdaFunctionCFName, "Arn") 593 customResource.UserProperties = resourceInfo.properties 594 template.AddResource(resourceInfo.logicalName(), customResource) 595 return nil 596 } 597 598 // END - customResourceInfo 599 //////////////////////////////////////////////////////////////////////////////// 600 601 // Interceptor is the type of an event interceptor that taps the event lifecycle 602 type Interceptor func(ctx context.Context, msg json.RawMessage) context.Context 603 604 // NamedInterceptor represents a named interceptor that's invoked in the event path 605 type NamedInterceptor struct { 606 Name string 607 Interceptor Interceptor 608 } 609 610 // InterceptorList is a list of NamedInterceptors 611 type InterceptorList []*NamedInterceptor 612 613 //////////////////////////////////////////////////////////////////////////////// 614 // START - LambdaEventInterceptors 615 616 // LambdaEventInterceptors is the struct that stores event handlers that tap into 617 // the normal event dispatching workflow 618 type LambdaEventInterceptors struct { 619 Begin InterceptorList 620 BeforeSetup InterceptorList 621 AfterSetup InterceptorList 622 BeforeDispatch InterceptorList 623 AfterDispatch InterceptorList 624 Complete InterceptorList 625 } 626 627 // Register is a convenience function to register a struct that 628 // implements the LambdaInterceptorProvider interface 629 func (lei *LambdaEventInterceptors) Register(provider LambdaInterceptorProvider) *LambdaEventInterceptors { 630 namedInterceptor := func(interceptor Interceptor) *NamedInterceptor { 631 return &NamedInterceptor{ 632 Name: fmt.Sprintf("%T", provider), 633 Interceptor: interceptor, 634 } 635 } 636 if lei.Begin == nil { 637 lei.Begin = make(InterceptorList, 0) 638 } 639 lei.Begin = append(lei.Begin, namedInterceptor(provider.Begin)) 640 641 if lei.BeforeSetup == nil { 642 lei.BeforeSetup = make(InterceptorList, 0) 643 } 644 lei.BeforeSetup = append(lei.BeforeSetup, namedInterceptor(provider.BeforeSetup)) 645 646 if lei.AfterSetup == nil { 647 lei.AfterSetup = make(InterceptorList, 0) 648 } 649 lei.AfterSetup = append(lei.AfterSetup, namedInterceptor(provider.AfterSetup)) 650 651 if lei.BeforeDispatch == nil { 652 lei.BeforeDispatch = make(InterceptorList, 0) 653 } 654 lei.BeforeDispatch = append(lei.BeforeDispatch, namedInterceptor(provider.BeforeDispatch)) 655 656 if lei.AfterDispatch == nil { 657 lei.AfterDispatch = make(InterceptorList, 0) 658 } 659 lei.AfterDispatch = append(lei.AfterDispatch, namedInterceptor(provider.AfterDispatch)) 660 661 if lei.Complete == nil { 662 lei.Complete = make(InterceptorList, 0) 663 } 664 lei.Complete = append(lei.Complete, namedInterceptor(provider.Complete)) 665 return lei 666 } 667 668 // LambdaInterceptorProvider is the interface that defines an event interceptor 669 // Interceptors are able to hook into the normal event processing pipeline 670 type LambdaInterceptorProvider interface { 671 Begin(ctx context.Context, msg json.RawMessage) context.Context 672 BeforeSetup(ctx context.Context, msg json.RawMessage) context.Context 673 AfterSetup(ctx context.Context, msg json.RawMessage) context.Context 674 BeforeDispatch(ctx context.Context, msg json.RawMessage) context.Context 675 AfterDispatch(ctx context.Context, msg json.RawMessage) context.Context 676 Complete(ctx context.Context, msg json.RawMessage) context.Context 677 } 678 679 //////////////////////////////////////////////////////////////////////////////// 680 // START - LambdaAWSInfo 681 682 // LambdaAWSInfo stores all data necessary to provision a golang-based AWS Lambda function. 683 type LambdaAWSInfo struct { 684 // AWS Go lambda compliant function 685 handlerSymbol interface{} 686 // pointer to lambda function 687 //lambdaFn LambdaFunction 688 // The user supplied internal name 689 userSuppliedFunctionName string 690 // Role name (NOT ARN) to use during AWS Lambda Execution. See 691 // the FunctionConfiguration (http://docs.aws.amazon.com/lambda/latest/dg/API_FunctionConfiguration.html) 692 // docs for more info. 693 // Note that either `RoleName` or `RoleDefinition` must be supplied 694 RoleName string 695 // IAM Role Definition if the stack should implicitly create an IAM role for 696 // lambda execution. Note that either `RoleName` or `RoleDefinition` must be supplied 697 RoleDefinition *IAMRoleDefinition 698 // Additional exeuction options 699 Options *LambdaFunctionOptions 700 // Permissions to enable push-based Lambda execution. See the 701 // Permission Model docs (http://docs.aws.amazon.com/lambda/latest/dg/intro-permission-model.html) 702 // for more information. 703 Permissions []LambdaPermissionExporter 704 // EventSource mappings to enable for pull-based Lambda execution. See the 705 // Event Source docs (http://docs.aws.amazon.com/lambda/latest/dg/intro-core-components.html) 706 // for more information 707 EventSourceMappings []*EventSourceMapping 708 // Template decorators. If non empty, the decorators will be called, 709 // in order, to annotate the template 710 Decorators []TemplateDecoratorHandler 711 // Template decorator. If defined, the decorator will be called to insert additional 712 // resources on behalf of this lambda function 713 Decorator TemplateDecorator 714 // Optional array of infrastructure resource logical names, typically 715 // defined by a TemplateDecorator, that this lambda depends on 716 DependsOn []string 717 718 // Lambda Layers 719 // Ref: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html#cfn-lambda-function-layers 720 Layers []gocf.Stringable 721 722 // Slice of customResourceInfo pointers for any associated CloudFormation 723 // CustomResources associated with this lambda 724 customResources []*customResourceInfo 725 // Cached lambda name s.t. we only compute it once 726 cachedLambdaFunctionName string 727 728 // deprecation notices 729 deprecationNotices []string 730 731 // interceptors 732 Interceptors *LambdaEventInterceptors 733 } 734 735 // lambdaFunctionName returns the internal 736 // function name for lambda export binding 737 func (info *LambdaAWSInfo) lambdaFunctionName() string { 738 if info.cachedLambdaFunctionName != "" { 739 return info.cachedLambdaFunctionName 740 } 741 var lambdaFuncName string 742 743 if info.Options != nil && 744 info.Options.SpartaOptions != nil && 745 info.Options.SpartaOptions.Name != "" { 746 lambdaFuncName = info.Options.SpartaOptions.Name 747 } else if info.userSuppliedFunctionName != "" { 748 lambdaFuncName = info.userSuppliedFunctionName 749 } else { 750 // Using the default name, let's at least remove the 751 // first prefix, since that's the SCM provider and 752 // doesn't provide a lot of value... 753 754 if info.handlerSymbol != nil { 755 lambdaPtr := runtime.FuncForPC(reflect.ValueOf(info.handlerSymbol).Pointer()) 756 lambdaFuncName = lambdaPtr.Name() 757 } 758 759 // Split 760 // cwd: /Users/mweagle/Documents/gopath/src/github.com/mweagle/SpartaHelloWorld 761 // anonymous: github.com/mweagle/Sparta.(*StructHandler1).(github.com/mweagle/Sparta.handler)-fm 762 // RE==> var reSplit = regexp.MustCompile("[\\(\\)\\.\\*]+") 763 // RESULT ==> Hello,[github com/mweagle/Sparta StructHandler1 github com/mweagle/Sparta handler -fm] 764 // Same package: main.helloWorld 765 // Other package, free function: github.com/mweagle/SpartaPython.HelloWorld 766 767 // Grab the name of the function... 768 structDefined := strings.Contains(lambdaFuncName, "(") || strings.Contains(lambdaFuncName, ")") 769 otherPackage := strings.Contains(lambdaFuncName, "/") 770 canonicalName := lambdaFuncName 771 if structDefined { 772 var reSplit = regexp.MustCompile(`[*\(\)\[\]]+`) 773 // Function name: 774 // github.com/mweagle/Sparta.(*StructHandler1).handler-fm 775 parts := reSplit.Split(lambdaFuncName, -1) 776 lastPart := parts[len(parts)-1] 777 penultimatePart := lastPart 778 if len(parts) > 1 { 779 penultimatePart = parts[len(parts)-2] 780 } 781 intermediateName := fmt.Sprintf("%s-%s", penultimatePart, lastPart) 782 reClean := regexp.MustCompile(`[\*\(\)]+`) 783 canonicalName = reClean.ReplaceAllString(intermediateName, "") 784 } else if otherPackage { 785 parts := strings.Split(lambdaFuncName, "/") 786 canonicalName = parts[len(parts)-1] 787 } 788 // Final sanitization 789 // Issue: https://github.com/mweagle/Sparta/issues/63 790 lambdaFuncName = sanitizedName(canonicalName) 791 } 792 // Cache it so we only do this once 793 info.cachedLambdaFunctionName = lambdaFuncName 794 return info.cachedLambdaFunctionName 795 } 796 797 // NewDescriptionTriplet returns a decription triplet where this lambda 798 // is either a sink or a source 799 func (info *LambdaAWSInfo) NewDescriptionTriplet(nodeName string, lambdaIsTarget bool) *DescriptionTriplet { 800 801 if lambdaIsTarget { 802 return &DescriptionTriplet{ 803 SourceNodeName: nodeName, 804 TargetNodeName: info.lambdaFunctionName(), 805 } 806 } 807 return &DescriptionTriplet{ 808 SourceNodeName: info.lambdaFunctionName(), 809 TargetNodeName: nodeName, 810 } 811 } 812 813 // Description satisfies the Describable interface 814 func (info *LambdaAWSInfo) Description(targetNodeName string) ([]*DescriptionTriplet, error) { 815 816 descriptionNodes := make([]*DescriptionTriplet, 0) 817 descriptionNodes = append(descriptionNodes, &DescriptionTriplet{ 818 SourceNodeName: info.lambdaFunctionName(), 819 DisplayInfo: &DescriptionDisplayInfo{ 820 SourceIcon: &DescriptionIcon{ 821 Category: "Compute", 822 Name: "AWS-Lambda@4x.png", 823 }, 824 }, 825 TargetNodeName: targetNodeName, 826 }) 827 // What about the permissions? 828 for _, eachPermission := range info.Permissions { 829 nodes, err := eachPermission.descriptionInfo() 830 if nil != err { 831 return nil, err 832 } 833 834 for _, eachNode := range nodes { 835 name := strings.TrimSpace(eachNode.Name) 836 arc := strings.TrimSpace(eachNode.Relation) 837 descriptionNodes = append(descriptionNodes, &DescriptionTriplet{ 838 SourceNodeName: name, 839 DisplayInfo: &DescriptionDisplayInfo{ 840 SourceNodeColor: nodeColorEventSource, 841 SourceIcon: iconForAWSResource(name), 842 }, 843 ArcLabel: arc, 844 TargetNodeName: info.lambdaFunctionName(), 845 }) 846 } 847 } 848 849 // Finally, event sources... 850 for index, eachEventSourceMapping := range info.EventSourceMappings { 851 dynamicArn := spartaCF.DynamicValueToStringExpr(eachEventSourceMapping.EventSourceArn) 852 jsonBytes, jsonBytesErr := json.Marshal(dynamicArn) 853 if jsonBytesErr != nil { 854 jsonBytes = []byte(fmt.Sprintf("%s-EventSourceMapping[%d]", 855 info.lambdaFunctionName(), 856 index)) 857 } 858 nodeName := string(jsonBytes) 859 descriptionNodes = append(descriptionNodes, &DescriptionTriplet{ 860 SourceNodeName: nodeName, 861 DisplayInfo: &DescriptionDisplayInfo{ 862 SourceIcon: iconForAWSResource(dynamicArn), 863 }, 864 TargetNodeName: info.lambdaFunctionName(), 865 }) 866 } 867 return descriptionNodes, nil 868 } 869 870 // RequireCustomResource adds a Lambda-backed CustomResource entry to the CloudFormation 871 // template. This function will be made a dependency of the owning Lambda function. 872 // The returned string is the custom resource's CloudFormation logical resource 873 // name that can be used for `Fn:GetAtt` calls for metadata lookups 874 func (info *LambdaAWSInfo) RequireCustomResource(roleNameOrIAMRoleDefinition interface{}, 875 handlerSymbol interface{}, 876 lambdaOptions *LambdaFunctionOptions, 877 resourceProps map[string]interface{}) (string, error) { 878 if nil == handlerSymbol { 879 return "", fmt.Errorf("RequireCustomResource userFunc must not be nil") 880 } 881 // Is it valid? 882 // Get the function pointer for this... 883 handlerType := reflect.TypeOf(handlerSymbol) 884 if handlerType.Kind() != reflect.Func { 885 return "", fmt.Errorf("CustomResourceHandler kind %s is not %s", 886 handlerType.Kind(), 887 reflect.Func) 888 } 889 890 if nil == lambdaOptions { 891 lambdaOptions = defaultLambdaFunctionOptions() 892 } 893 funcPtr := runtime.FuncForPC(reflect.ValueOf(handlerSymbol).Pointer()) 894 resourceInfo := &customResourceInfo{ 895 handlerSymbol: handlerSymbol, 896 userFunctionName: funcPtr.Name(), 897 options: lambdaOptions, 898 properties: resourceProps, 899 } 900 switch v := roleNameOrIAMRoleDefinition.(type) { 901 case string: 902 resourceInfo.roleName = roleNameOrIAMRoleDefinition.(string) 903 case IAMRoleDefinition: 904 definition := roleNameOrIAMRoleDefinition.(IAMRoleDefinition) 905 resourceInfo.roleDefinition = &definition 906 default: 907 panic(fmt.Sprintf("Unsupported IAM Role type: %s", v)) 908 } 909 resourceInfo.options.Environment = make(map[string]*gocf.StringExpr) 910 info.customResources = append(info.customResources, resourceInfo) 911 info.DependsOn = append(info.DependsOn, resourceInfo.logicalName()) 912 return resourceInfo.logicalName(), nil 913 } 914 915 // LogicalResourceName returns the stable, content-addressable logical 916 // name for this LambdaAWSInfo value. This is the CloudFormation 917 // resource name 918 func (info *LambdaAWSInfo) LogicalResourceName() string { 919 // Per http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resources-section-structure.html, 920 // we can only use alphanumeric, so we'll take the sanitized name and 921 // remove all underscores 922 // Prefer the user-supplied stable name to the internal one. 923 baseName := info.lambdaFunctionName() 924 resourceName := strings.Replace(sanitizedName(baseName), "_", "", -1) 925 prefix := fmt.Sprintf("%sLambda", resourceName) 926 return CloudFormationResourceName(prefix, info.lambdaFunctionName()) 927 } 928 929 func (info *LambdaAWSInfo) applyDecorators(template *gocf.Template, 930 lambdaResource gocf.LambdaFunction, 931 cfResource *gocf.Resource, 932 serviceName string, 933 S3Bucket string, 934 S3Key string, 935 buildID string, 936 context map[string]interface{}, 937 logger *logrus.Logger) error { 938 939 decorators := info.Decorators 940 if info.Decorator != nil { 941 logger.Debug("Decorator found for Lambda: ", info.lambdaFunctionName()) 942 logger.Warn("DEPRECATED: Single `Decorator` field is superseded by `Decorators` slice") 943 decorators = append(decorators, TemplateDecoratorHookFunc(info.Decorator)) 944 } 945 946 for _, eachDecorator := range decorators { 947 // Create an empty template so that we can track whether things 948 // are overwritten 949 metadataMap := make(map[string]interface{}) 950 decoratorProxyTemplate := gocf.NewTemplate() 951 decoratorErr := eachDecorator.DecorateTemplate(serviceName, 952 info.LogicalResourceName(), 953 lambdaResource, 954 metadataMap, 955 S3Bucket, 956 S3Key, 957 buildID, 958 decoratorProxyTemplate, 959 context, 960 logger) 961 if decoratorErr != nil { 962 // Can we get the name? 963 decoratorName := fmt.Sprintf("%T", eachDecorator) 964 errorValue := errors.Errorf("TemplateDecorator %s failed to apply. Error: %s", 965 decoratorName, 966 decoratorErr) 967 return errorValue 968 } 969 // This data is marshalled into a DiscoveryInfo struct s.t. it can be 970 // unmarshalled via sparta.Discover. We're going to just stuff it into 971 // it's own same named property 972 if len(metadataMap) != 0 { 973 safeMetadataInsert(cfResource, info.LogicalResourceName(), metadataMap) 974 } 975 // Append the custom resources 976 safeMergeErrs := gocc.SafeMerge(decoratorProxyTemplate, 977 template) 978 if len(safeMergeErrs) != 0 { 979 return errors.Errorf("Lambda (%s) decorator created conflicting resources: %v", 980 info.lambdaFunctionName(), 981 safeMergeErrs) 982 } 983 } 984 return nil 985 } 986 987 // Marshal this object into 1 or more CloudFormation resource definitions that are accumulated 988 // in the resources map 989 func (info *LambdaAWSInfo) export(serviceName string, 990 S3Bucket string, 991 S3Key string, 992 S3Version string, 993 buildID string, 994 roleNameMap map[string]*gocf.StringExpr, 995 template *gocf.Template, 996 context map[string]interface{}, 997 logger *logrus.Logger) error { 998 999 // Let's make sure the handler has the proper signature...This is basically 1000 // copy-pasted from the SDK 1001 1002 // If we have RoleName, then get the ARN, otherwise get the Ref 1003 var dependsOn []string 1004 if nil != info.DependsOn { 1005 dependsOn = append(dependsOn, info.DependsOn...) 1006 } 1007 1008 iamRoleArnName := info.RoleName 1009 1010 // If there is no user supplied role, that means that the associated 1011 // IAMRoleDefinition name has been created and this resource needs to 1012 // depend on that being created. 1013 if iamRoleArnName == "" && info.RoleDefinition != nil { 1014 iamRoleArnName = info.RoleDefinition.logicalName(serviceName, info.lambdaFunctionName()) 1015 dependsOn = append(dependsOn, info.RoleDefinition.logicalName(serviceName, info.lambdaFunctionName())) 1016 } 1017 lambdaDescription := info.Options.Description 1018 if lambdaDescription == "" { 1019 lambdaDescription = fmt.Sprintf("%s: %s", serviceName, info.lambdaFunctionName()) 1020 } 1021 1022 // Create the primary resource 1023 lambdaResource := gocf.LambdaFunction{ 1024 Code: &gocf.LambdaFunctionCode{ 1025 S3Bucket: gocf.String(S3Bucket), 1026 S3Key: gocf.String(S3Key), 1027 }, 1028 Description: gocf.String(lambdaDescription), 1029 Handler: gocf.String(SpartaBinaryName), 1030 MemorySize: gocf.Integer(info.Options.MemorySize), 1031 Role: roleNameMap[iamRoleArnName], 1032 Runtime: gocf.String(GoLambdaVersion), 1033 Timeout: gocf.Integer(info.Options.Timeout), 1034 VPCConfig: info.Options.VpcConfig, 1035 } 1036 // Layers? 1037 if nil != info.Layers { 1038 lambdaResource.Layers = gocf.StringList(info.Layers...) 1039 } 1040 1041 if S3Version != "" { 1042 lambdaResource.Code.S3ObjectVersion = gocf.String(S3Version) 1043 } 1044 if info.Options.ReservedConcurrentExecutions != 0 { 1045 lambdaResource.ReservedConcurrentExecutions = gocf.Integer(info.Options.ReservedConcurrentExecutions) 1046 } 1047 if info.Options.DeadLetterConfigArn != nil { 1048 lambdaResource.DeadLetterConfig = &gocf.LambdaFunctionDeadLetterConfig{ 1049 TargetArn: info.Options.DeadLetterConfigArn.String(), 1050 } 1051 } 1052 if nil != info.Options.TracingConfig { 1053 lambdaResource.TracingConfig = info.Options.TracingConfig 1054 } 1055 if info.Options.KmsKeyArn != "" { 1056 lambdaResource.KmsKeyArn = gocf.String(info.Options.KmsKeyArn) 1057 } 1058 if nil != info.Options.Tags { 1059 tagList := gocf.TagList{} 1060 for eachKey, eachValue := range info.Options.Tags { 1061 tagList = append(tagList, gocf.Tag{ 1062 Key: gocf.String(eachKey), 1063 Value: gocf.String(eachValue), 1064 }) 1065 } 1066 lambdaResource.Tags = &tagList 1067 } 1068 1069 // DISPATCH INFORMATION 1070 // Make sure we set the environment variable that 1071 // tells us which function to actually execute in 1072 // execute_awsbinary.go 1073 if info.Options.Environment == nil { 1074 info.Options.Environment = make(map[string]*gocf.StringExpr) 1075 } 1076 info.Options.Environment[envVarLogLevel] = 1077 gocf.String(logger.Level.String()) 1078 1079 lambdaResource.Environment = &gocf.LambdaFunctionEnvironment{ 1080 Variables: info.Options.Environment, 1081 } 1082 1083 // This function name is set here to be the same 1084 // name that the dispatcher will look up in execute 1085 // using the same logic so that we can borrow the 1086 // `AWS_LAMBDA_FUNCTION_NAME` env var 1087 lambdaFunctionName := awsLambdaFunctionName(info.lambdaFunctionName()) 1088 lambdaResource.FunctionName = lambdaFunctionName.String() 1089 1090 cfResource := template.AddResource(info.LogicalResourceName(), lambdaResource) 1091 cfResource.DependsOn = append(cfResource.DependsOn, dependsOn...) 1092 safeMetadataInsert(cfResource, "golangFunc", info.lambdaFunctionName()) 1093 1094 // Create the lambda Ref in case we need a permission or event mapping 1095 functionAttr := gocf.GetAtt(info.LogicalResourceName(), "Arn") 1096 1097 // Permissions 1098 for _, eachPermission := range info.Permissions { 1099 _, err := eachPermission.export(serviceName, 1100 info.lambdaFunctionName(), 1101 info.LogicalResourceName(), 1102 template, 1103 S3Bucket, 1104 S3Key, 1105 logger) 1106 if nil != err { 1107 return errors.Wrapf(err, "Failed to export lambda permission") 1108 } 1109 } 1110 1111 // Event Source Mappings 1112 for _, eachEventSourceMapping := range info.EventSourceMappings { 1113 mappingErr := eachEventSourceMapping.export(serviceName, 1114 info.lambdaFunctionName(), 1115 functionAttr, 1116 S3Bucket, 1117 S3Key, 1118 template, 1119 logger) 1120 if nil != mappingErr { 1121 return mappingErr 1122 } 1123 } 1124 1125 // CustomResource 1126 for _, eachCustomResource := range info.customResources { 1127 1128 resourceErr := eachCustomResource.export(serviceName, 1129 functionAttr, 1130 S3Bucket, 1131 S3Key, 1132 roleNameMap, 1133 template, 1134 logger) 1135 if nil != resourceErr { 1136 return resourceErr 1137 } 1138 } 1139 1140 decoratorErr := info.applyDecorators(template, 1141 lambdaResource, 1142 cfResource, 1143 serviceName, 1144 S3Bucket, 1145 S3Key, 1146 buildID, 1147 context, 1148 logger) 1149 1150 if decoratorErr != nil { 1151 return decoratorErr 1152 } 1153 1154 // Log any deprecation notices 1155 for _, eachDeprecation := range info.deprecationNotices { 1156 logger.Warn(eachDeprecation) 1157 } 1158 return nil 1159 } 1160 1161 // 1162 // END - LambdaAWSInfo 1163 //////////////////////////////////////////////////////////////////////////////// 1164 1165 //////////////////////////////////////////////////////////////////////////////// 1166 // 1167 // BEGIN - Private 1168 // 1169 1170 func validateSpartaPreconditions(lambdaAWSInfos []*LambdaAWSInfo, 1171 logger *logrus.Logger) error { 1172 1173 var errorText []string 1174 collisionMemo := make(map[string]int) 1175 1176 incrementCounter := func(keyName string) { 1177 _, exists := collisionMemo[keyName] 1178 if !exists { 1179 collisionMemo[keyName] = 1 1180 } else { 1181 collisionMemo[keyName] = collisionMemo[keyName] + 1 1182 } 1183 } 1184 // 0 - check for nil 1185 for eachIndex, eachLambda := range lambdaAWSInfos { 1186 if eachLambda == nil { 1187 errorText = append(errorText, 1188 fmt.Sprintf("Lambda at position %d is `nil`", eachIndex)) 1189 } 1190 } 1191 // Semantic checks only iff lambdas are non-nil 1192 if len(errorText) == 0 { 1193 1194 // 1 - check for invalid signatures 1195 for _, eachLambda := range lambdaAWSInfos { 1196 validationErr := ensureValidSignature(eachLambda.userSuppliedFunctionName, 1197 eachLambda.handlerSymbol) 1198 if validationErr != nil { 1199 errorText = append(errorText, validationErr.Error()) 1200 } 1201 } 1202 1203 // 2 - check for duplicate golang function references. 1204 for _, eachLambda := range lambdaAWSInfos { 1205 incrementCounter(eachLambda.lambdaFunctionName()) 1206 for _, eachCustom := range eachLambda.customResources { 1207 incrementCounter(eachCustom.userFunctionName) 1208 } 1209 } 1210 // Duplicates? 1211 for eachLambdaName, eachCount := range collisionMemo { 1212 if eachCount > 1 { 1213 logger.WithFields(logrus.Fields{ 1214 "CollisionCount": eachCount, 1215 "Name": eachLambdaName, 1216 }).Error("NewAWSLambda") 1217 errorText = append(errorText, 1218 fmt.Sprintf("Multiple definitions of lambda: %s", eachLambdaName)) 1219 } 1220 } 1221 logger.WithFields(logrus.Fields{ 1222 "CollisionMap": collisionMemo, 1223 }).Debug("Lambda collision map") 1224 } 1225 if len(errorText) != 0 { 1226 return errors.New(strings.Join(errorText[:], "\n")) 1227 } 1228 1229 return nil 1230 } 1231 1232 // Sanitize the provided input by replacing illegal characters with underscores 1233 func sanitizedName(input string) string { 1234 return reSanitize.ReplaceAllString(input, "_") 1235 } 1236 1237 // 1238 // END - Private 1239 // 1240 //////////////////////////////////////////////////////////////////////////////// 1241 1242 //////////////////////////////////////////////////////////////////////////////// 1243 // Public 1244 //////////////////////////////////////////////////////////////////////////////// 1245 1246 // AWSLambdaProvider is an interface that represents a struct that 1247 // encapsulates a Lambda function 1248 type AWSLambdaProvider interface { 1249 Handler() interface{} 1250 Name() string 1251 Role() interface{} 1252 } 1253 1254 // CloudFormationResourceName returns a name suitable as a logical 1255 // CloudFormation resource value. See http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resources-section-structure.html 1256 // for more information. The `prefix` value should provide a hint as to the 1257 // resource type (eg, `SNSConfigurator`, `ImageTranscoder`). Note that the returned 1258 // name is not content-addressable. 1259 func CloudFormationResourceName(prefix string, parts ...string) string { 1260 return spartaCF.CloudFormationResourceName(prefix, parts...) 1261 } 1262 1263 // LambdaName returns the Go-reflection discovered name for a given 1264 // function 1265 func LambdaName(handlerSymbol interface{}) string { 1266 funcPtr := runtime.FuncForPC(reflect.ValueOf(handlerSymbol).Pointer()) 1267 return funcPtr.Name() 1268 } 1269 1270 // NewAWSLambdaFromProvider is a utility function to return 1271 // an LambdaAWSInfo from an AWSLambdaProvider 1272 func NewAWSLambdaFromProvider(provider AWSLambdaProvider) (*LambdaAWSInfo, error) { 1273 return NewAWSLambda(provider.Name(), 1274 provider.Handler(), 1275 provider.Role()) 1276 } 1277 1278 /* 1279 Supported lambdaHandler signatures: 1280 1281 • func () 1282 • func () error 1283 • func (TIn), error 1284 • func () (TOut, error) 1285 • func (context.Context) error 1286 • func (context.Context, TIn) error 1287 • func (context.Context) (TOut, error) 1288 • func (context.Context, TIn) (TOut, error) 1289 */ 1290 1291 // NewAWSLambda is the creation function that replaces HandleAWSLambda. It returns 1292 // a *LambdaAWSInfo pointer to the struct representing the AWS lambda target. It's a 1293 // go-friendly signature for creating a lambda function 1294 func NewAWSLambda(functionName string, 1295 lambdaHandler interface{}, 1296 roleNameOrIAMRoleDefinition interface{}) (*LambdaAWSInfo, error) { 1297 1298 if functionName == "" { 1299 return nil, errors.Errorf("AWS Lambda function name must not be empty") 1300 } 1301 if lambdaHandler == nil { 1302 return nil, errors.Errorf("AWS Lambda function handler must not be nil") 1303 } 1304 1305 lambda := &LambdaAWSInfo{ 1306 userSuppliedFunctionName: functionName, 1307 handlerSymbol: lambdaHandler, 1308 Options: defaultLambdaFunctionOptions(), 1309 Permissions: make([]LambdaPermissionExporter, 0), 1310 EventSourceMappings: make([]*EventSourceMapping, 0), 1311 deprecationNotices: make([]string, 0), 1312 } 1313 1314 switch v := roleNameOrIAMRoleDefinition.(type) { 1315 case string: 1316 lambda.RoleName = v 1317 case IAMRoleDefinition: 1318 definition := v 1319 lambda.RoleDefinition = &definition 1320 default: 1321 return nil, errors.Errorf("AWS Lambda function IAM role must not be empty") 1322 } 1323 return lambda, nil 1324 } 1325 1326 // HandleAWSLambda is deprecated in favor of NewAWSLambda(...) 1327 func HandleAWSLambda(functionName string, 1328 lambdaHandler interface{}, 1329 roleNameOrIAMRoleDefinition interface{}) *LambdaAWSInfo { 1330 1331 lambda, lambdaErr := NewAWSLambda(functionName, lambdaHandler, roleNameOrIAMRoleDefinition) 1332 if lambdaErr != nil { 1333 panic(lambdaErr) 1334 } 1335 lambda.deprecationNotices = append(lambda.deprecationNotices, "sparta.HandleAWSLambda is deprecated starting with v1.6.0. Prefer `sparta.NewAWSLambda(...) (*LambdaAWSInfo, error)`") 1336 return lambda 1337 } 1338 1339 // IsExecutingInLambda is a utility function to return a boolean 1340 // indicating whether the application is running in AWS Lambda. 1341 // See the list of environment variables defined at: 1342 // https://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html 1343 // for more information. 1344 func IsExecutingInLambda() bool { 1345 return os.Getenv("LAMBDA_TASK_ROOT") != "" || 1346 os.Getenv("AWS_EXECUTION_ENV") != "" 1347 }