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  }