func parseFnJoinExpr(data map[string]interface{}) (*gocf.StringExpr, error) { if len(data) <= 0 { return nil, fmt.Errorf("FnJoinExpr data is empty") } for eachKey, eachValue := range data { switch eachKey { case "Ref": return gocf.Ref(eachValue.(string)).String(), nil case "Fn::GetAtt": attrValues, attrValuesErr := toExpressionSlice(eachValue) if nil != attrValuesErr { return nil, attrValuesErr } if len(attrValues) != 2 { return nil, fmt.Errorf("Invalid params for Fn::GetAtt: %s", eachValue) } return gocf.GetAtt(attrValues[0], attrValues[1]).String(), nil case "Fn::FindInMap": attrValues, attrValuesErr := toExpressionSlice(eachValue) if nil != attrValuesErr { return nil, attrValuesErr } if len(attrValues) != 3 { return nil, fmt.Errorf("Invalid params for Fn::FindInMap: %s", eachValue) } return gocf.FindInMap(attrValues[0], gocf.String(attrValues[1]), gocf.String(attrValues[2])), nil } } return nil, fmt.Errorf("Unsupported AWS Function detected: %#v", data) }
func integrationResponses(userResponses map[int]*IntegrationResponse, corsEnabled bool) *gocf.APIGatewayMethodIntegrationIntegrationResponseList { var integrationResponses gocf.APIGatewayMethodIntegrationIntegrationResponseList // We've already populated this entire map in the NewMethod call for eachHTTPStatusCode, eachMethodIntegrationResponse := range userResponses { responseParameters := eachMethodIntegrationResponse.Parameters if corsEnabled { for eachKey, eachValue := range corsIntegrationResponseParams() { responseParameters[eachKey] = eachValue } } integrationResponse := gocf.APIGatewayMethodIntegrationIntegrationResponse{ ResponseTemplates: eachMethodIntegrationResponse.Templates, SelectionPattern: gocf.String(eachMethodIntegrationResponse.SelectionPattern), StatusCode: gocf.String(strconv.Itoa(eachHTTPStatusCode)), } if len(responseParameters) != 0 { integrationResponse.ResponseParameters = responseParameters } integrationResponses = append(integrationResponses, integrationResponse) } return &integrationResponses }
func (resourceInfo *customResourceInfo) export(serviceName string, targetLambda *gocf.StringExpr, S3Bucket string, S3Key string, roleNameMap map[string]*gocf.StringExpr, template *gocf.Template, logger *logrus.Logger) error { // Figure out the role name iamRoleArnName := resourceInfo.roleName // If there is no user supplied role, that means that the associated // IAMRoleDefinition name has been created and this resource needs to // depend on that being created. if iamRoleArnName == "" && resourceInfo.roleDefinition != nil { iamRoleArnName = resourceInfo.roleDefinition.logicalName(serviceName, resourceInfo.userFunctionName) } lambdaDescription := resourceInfo.options.Description if "" == lambdaDescription { lambdaDescription = fmt.Sprintf("%s CustomResource: %s", serviceName, resourceInfo.userFunctionName) } // Create the Lambda Function lambdaResource := gocf.LambdaFunction{ Code: &gocf.LambdaFunctionCode{ S3Bucket: gocf.String(S3Bucket), S3Key: gocf.String(S3Key), }, Description: gocf.String(lambdaDescription), Handler: gocf.String(fmt.Sprintf("index.%s", resourceInfo.jsHandlerName())), MemorySize: gocf.Integer(resourceInfo.options.MemorySize), Role: roleNameMap[iamRoleArnName], Runtime: gocf.String(NodeJSVersion), Timeout: gocf.Integer(resourceInfo.options.Timeout), VpcConfig: resourceInfo.options.VpcConfig, } lambdaFunctionCFName := CloudFormationResourceName("CustomResourceLambda", resourceInfo.userFunctionName, resourceInfo.logicalName()) cfResource := template.AddResource(lambdaFunctionCFName, lambdaResource) safeMetadataInsert(cfResource, "golangFunc", resourceInfo.userFunctionName) // And create the CustomResource that actually invokes it... newResource, newResourceError := newCloudFormationResource(cloudFormationLambda, logger) if nil != newResourceError { return newResourceError } customResource := newResource.(*cloudFormationLambdaCustomResource) customResource.ServiceToken = gocf.GetAtt(lambdaFunctionCFName, "Arn") customResource.UserProperties = resourceInfo.properties template.AddResource(resourceInfo.logicalName(), customResource) return nil }
func (rule *ReceiptRule) toResourceRule(serviceName string, functionArnRef interface{}, messageBodyStorage *MessageBodyStorage) *cloudformationresources.SESLambdaEventSourceResourceRule { resourceRule := &cloudformationresources.SESLambdaEventSourceResourceRule{ Name: gocf.String(rule.Name), ScanEnabled: gocf.Bool(!rule.ScanDisabled), Enabled: gocf.Bool(!rule.Disabled), Actions: make([]*cloudformationresources.SESLambdaEventSourceResourceAction, 0), Recipients: make([]*gocf.StringExpr, 0), } for _, eachRecipient := range rule.Recipients { resourceRule.Recipients = append(resourceRule.Recipients, gocf.String(eachRecipient)) } if "" != rule.TLSPolicy { resourceRule.TLSPolicy = gocf.String(rule.TLSPolicy) } // If there is a MessageBodyStorage reference, push that S3Action // to the head of the Actions list if nil != messageBodyStorage && !rule.BodyStorageOptions.DisableStorage { s3Action := &cloudformationresources.SESLambdaEventSourceResourceAction{ ActionType: gocf.String("S3Action"), ActionProperties: map[string]interface{}{ "BucketName": messageBodyStorage.bucketNameExpr, }, } if "" != rule.BodyStorageOptions.ObjectKeyPrefix { s3Action.ActionProperties["ObjectKeyPrefix"] = rule.BodyStorageOptions.ObjectKeyPrefix } if "" != rule.BodyStorageOptions.KmsKeyArn { s3Action.ActionProperties["KmsKeyArn"] = rule.BodyStorageOptions.KmsKeyArn } if "" != rule.BodyStorageOptions.TopicArn { s3Action.ActionProperties["TopicArn"] = rule.BodyStorageOptions.TopicArn } resourceRule.Actions = append(resourceRule.Actions, s3Action) } // There's always a lambda action lambdaAction := &cloudformationresources.SESLambdaEventSourceResourceAction{ ActionType: gocf.String("LambdaAction"), ActionProperties: map[string]interface{}{ "FunctionArn": functionArnRef, }, } lambdaAction.ActionProperties["InvocationType"] = rule.InvocationType if "" == rule.InvocationType { lambdaAction.ActionProperties["InvocationType"] = "Event" } if "" != rule.TopicArn { lambdaAction.ActionProperties["TopicArn"] = rule.TopicArn } resourceRule.Actions = append(resourceRule.Actions, lambdaAction) return resourceRule }
func (roleDefinition *IAMRoleDefinition) toResource(eventSourceMappings []*EventSourceMapping, options *LambdaFunctionOptions, logger *logrus.Logger) gocf.IAMRole { statements := CommonIAMStatements.Core for _, eachPrivilege := range roleDefinition.Privileges { statements = append(statements, spartaIAM.PolicyStatement{ Effect: "Allow", Action: eachPrivilege.Actions, Resource: eachPrivilege.resourceExpr(), }) } // Add VPC permissions iff needed if options != nil && options.VpcConfig != nil { for _, eachStatement := range CommonIAMStatements.VPC { statements = append(statements, eachStatement) } } // http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html for _, eachEventSourceMapping := range eventSourceMappings { arnParts := strings.Split(eachEventSourceMapping.EventSourceArn, ":") // 3rd slot is service scope if len(arnParts) >= 2 { awsService := arnParts[2] logger.Debug("Looking up common IAM privileges for EventSource: ", awsService) switch awsService { case "dynamodb": statements = append(statements, CommonIAMStatements.DynamoDB...) case "kinesis": for _, statement := range CommonIAMStatements.Kinesis { statement.Resource = gocf.String(eachEventSourceMapping.EventSourceArn) statements = append(statements, statement) } default: logger.Debug("No additional statements found") } } } iamPolicies := gocf.IAMPoliciesList{} iamPolicies = append(iamPolicies, gocf.IAMPolicies{ PolicyDocument: ArbitraryJSONObject{ "Version": "2012-10-17", "Statement": statements, }, PolicyName: gocf.String(CloudFormationResourceName("LambdaPolicy")), }) return gocf.IAMRole{ AssumeRolePolicyDocument: AssumePolicyDocument, Policies: &iamPolicies, } }
func annotateDiscoveryInfo(template *gocf.Template, logger *logrus.Logger) *gocf.Template { for eachResourceID, eachResource := range template.Resources { // Only apply this to lambda functions if eachResource.Properties.CfnResourceType() == "AWS::Lambda::Function" { // Update the metdata with a reference to the output of each // depended on item... for _, eachDependsKey := range eachResource.DependsOn { dependencyOutputs, _ := outputsForResource(template, eachDependsKey, logger) if nil != dependencyOutputs && len(dependencyOutputs) != 0 { logger.WithFields(logrus.Fields{ "Resource": eachDependsKey, "DependsOn": eachResource.DependsOn, "Outputs": dependencyOutputs, }).Debug("Resource metadata") safeMetadataInsert(eachResource, eachDependsKey, dependencyOutputs) } } // Also include standard AWS outputs at a resource level if a lambda // needs to self-discover other resources safeMetadataInsert(eachResource, TagLogicalResourceID, gocf.String(eachResourceID)) safeMetadataInsert(eachResource, TagStackRegion, gocf.Ref("AWS::Region")) safeMetadataInsert(eachResource, TagStackID, gocf.Ref("AWS::StackId")) safeMetadataInsert(eachResource, TagStackName, gocf.Ref("AWS::StackName")) } } return template }
func appendDynamicSNSLambda(api *sparta.API, lambdaFunctions []*sparta.LambdaAWSInfo) []*sparta.LambdaAWSInfo { snsTopicName := sparta.CloudFormationResourceName("SNSDynamicTopic") lambdaFn := sparta.NewLambda(sparta.IAMRoleDefinition{}, echoDynamicSNSEvent, nil) lambdaFn.Permissions = append(lambdaFn.Permissions, sparta.SNSPermission{ BasePermission: sparta.BasePermission{ SourceArn: gocf.Ref(snsTopicName), }, }) lambdaFn.Decorator = func(serviceName string, lambdaResourceName string, lambdaResource gocf.LambdaFunction, resourceMetadata map[string]interface{}, S3Bucket string, S3Key string, buildID string, template *gocf.Template, context map[string]interface{}, logger *logrus.Logger) error { template.AddResource(snsTopicName, &gocf.SNSTopic{ DisplayName: gocf.String("Sparta Application SNS topic"), }) return nil } return append(lambdaFunctions, lambdaFn) }
// S3ArnForBucket returns a CloudFormation-compatible Arn expression // (string or Ref) suitable for template reference. The bucket // parameter may be either a string or an interface{} ("Ref: "myResource") // value func S3ArnForBucket(bucket interface{}) *gocf.StringExpr { arnParts := []gocf.Stringable{gocf.String("arn:aws:s3:::")} switch bucket.(type) { case string: // Don't be smart if the Arn value is a user supplied literal arnParts = append(arnParts, gocf.String(bucket.(string))) case *gocf.StringExpr: arnParts = append(arnParts, bucket.(*gocf.StringExpr)) case gocf.RefFunc: arnParts = append(arnParts, bucket.(gocf.RefFunc).String()) default: panic(fmt.Sprintf("Unsupported SourceArn value type: %+v", bucket)) } return gocf.Join("", arnParts...).String() }
// DefaultMethodResponses returns the default set of Method HTTPStatus->Response // pass through responses. The successfulHTTPStatusCode param is the single // 2XX response code to use for the method. func methodResponses(userResponses map[int]*Response, corsEnabled bool) *gocf.APIGatewayMethodMethodResponseList { var responses gocf.APIGatewayMethodMethodResponseList for eachHTTPStatusCode, eachResponse := range userResponses { methodResponseParams := eachResponse.Parameters if corsEnabled { for eachString, eachBool := range corsMethodResponseParams() { methodResponseParams[eachString] = eachBool } } // Then transform them all to strings because internet methodResponseStringParams := make(map[string]string, len(methodResponseParams)) for eachKey, eachBool := range methodResponseParams { methodResponseStringParams[eachKey] = fmt.Sprintf("%t", eachBool) } methodResponse := gocf.APIGatewayMethodMethodResponse{ StatusCode: gocf.String(strconv.Itoa(eachHTTPStatusCode)), } if len(methodResponseStringParams) != 0 { methodResponse.ResponseParameters = methodResponseStringParams } responses = append(responses, methodResponse) } return &responses }
func templateDecorator(serviceName string, lambdaResourceName string, lambdaResource gocf.LambdaFunction, resourceMetadata map[string]interface{}, S3Bucket string, S3Key string, buildID string, cfTemplate *gocf.Template, context map[string]interface{}, logger *logrus.Logger) error { // Add an empty resource newResource, err := newCloudFormationResource("Custom::ProvisionTestEmpty", logger) if nil != err { return err } customResource := newResource.(*cloudFormationProvisionTestResource) customResource.ServiceToken = "arn:aws:sns:us-east-1:84969EXAMPLE:CRTest" customResource.TestKey = "Hello World" cfTemplate.AddResource("ProvisionTestResource", customResource) // Add an output cfTemplate.Outputs["OutputDecorationTest"] = &gocf.Output{ Description: "Information about the value", Value: gocf.String("My key"), } return nil }
func (rolePrivilege *IAMRolePrivilege) resourceExpr() *gocf.StringExpr { switch rolePrivilege.Resource.(type) { case string: return gocf.String(rolePrivilege.Resource.(string)) default: return rolePrivilege.Resource.(*gocf.StringExpr) } }
func (perm SNSPermission) export(serviceName string, lambdaFunctionDisplayName string, lambdaLogicalCFResourceName string, template *gocf.Template, S3Bucket string, S3Key string, logger *logrus.Logger) (string, error) { sourceArnExpression := perm.BasePermission.sourceArnExpr(snsSourceArnParts...) targetLambdaResourceName, err := perm.BasePermission.export(gocf.String(SNSPrincipal), snsSourceArnParts, lambdaFunctionDisplayName, lambdaLogicalCFResourceName, template, S3Bucket, S3Key, logger) if nil != err { return "", err } // Make sure the custom lambda that manages s3 notifications is provisioned. configuratorResName, err := ensureCustomResourceHandler(serviceName, cloudformationresources.SNSLambdaEventSource, sourceArnExpression, []string{}, template, S3Bucket, S3Key, logger) if nil != err { return "", err } // Add a custom resource invocation for this configuration ////////////////////////////////////////////////////////////////////////////// newResource, newResourceError := newCloudFormationResource(cloudformationresources.SNSLambdaEventSource, logger) if nil != newResourceError { return "", newResourceError } customResource := newResource.(*cloudformationresources.SNSLambdaEventSourceResource) customResource.ServiceToken = gocf.GetAtt(configuratorResName, "Arn") customResource.LambdaTargetArn = gocf.GetAtt(lambdaLogicalCFResourceName, "Arn") customResource.SNSTopicArn = sourceArnExpression // Name? resourceInvokerName := CloudFormationResourceName("ConfigSNS", lambdaLogicalCFResourceName, perm.BasePermission.SourceAccount) // Add it cfResource := template.AddResource(resourceInvokerName, customResource) cfResource.DependsOn = append(cfResource.DependsOn, targetLambdaResourceName, configuratorResName) return "", nil }
func appendDynamicS3BucketLambda(api *sparta.API, lambdaFunctions []*sparta.LambdaAWSInfo) []*sparta.LambdaAWSInfo { s3BucketResourceName := sparta.CloudFormationResourceName("S3DynamicBucket") lambdaFn := sparta.NewLambda(sparta.IAMRoleDefinition{}, echoS3DynamicBucketEvent, nil) lambdaFn.Permissions = append(lambdaFn.Permissions, sparta.S3Permission{ BasePermission: sparta.BasePermission{ SourceArn: gocf.Ref(s3BucketResourceName), }, Events: []string{"s3:ObjectCreated:*", "s3:ObjectRemoved:*"}, }) lambdaFn.DependsOn = append(lambdaFn.DependsOn, s3BucketResourceName) // Add permission s.t. the lambda function could read from the S3 bucket lambdaFn.RoleDefinition.Privileges = append(lambdaFn.RoleDefinition.Privileges, sparta.IAMRolePrivilege{ Actions: []string{"s3:GetObject", "s3:HeadObject"}, Resource: spartaCF.S3AllKeysArnForBucket(gocf.Ref(s3BucketResourceName)), }) lambdaFn.Decorator = func(serviceName string, lambdaResourceName string, lambdaResource gocf.LambdaFunction, resourceMetadata map[string]interface{}, S3Bucket string, S3Key string, buildID string, template *gocf.Template, context map[string]interface{}, logger *logrus.Logger) error { cfResource := template.AddResource(s3BucketResourceName, &gocf.S3Bucket{ AccessControl: gocf.String("PublicRead"), Tags: []gocf.ResourceTag{ gocf.ResourceTag{ Key: gocf.String("SpecialKey"), Value: gocf.String("SpecialValue"), }, }, }) cfResource.DeletionPolicy = "Delete" return nil } return append(lambdaFunctions, lambdaFn) }
func (perm BasePermission) export(principal *gocf.StringExpr, arnPrefixParts []gocf.Stringable, lambdaFunctionDisplayName string, lambdaLogicalCFResourceName string, template *gocf.Template, S3Bucket string, S3Key string, logger *logrus.Logger) (string, error) { lambdaPermission := gocf.LambdaPermission{ Action: gocf.String("lambda:InvokeFunction"), FunctionName: gocf.GetAtt(lambdaLogicalCFResourceName, "Arn"), Principal: principal, } // If the Arn isn't the wildcard value, then include it. if nil != perm.SourceArn { switch perm.SourceArn.(type) { case string: // Don't be smart if the Arn value is a user supplied literal if "*" != perm.SourceArn.(string) { lambdaPermission.SourceArn = gocf.String(perm.SourceArn.(string)) } default: lambdaPermission.SourceArn = perm.sourceArnExpr(arnPrefixParts...) } } if "" != perm.SourceAccount { lambdaPermission.SourceAccount = gocf.String(perm.SourceAccount) } arnLiteral, arnLiteralErr := json.Marshal(lambdaPermission.SourceArn) if nil != arnLiteralErr { return "", arnLiteralErr } resourceName := CloudFormationResourceName("LambdaPerm%s", principal.Literal, string(arnLiteral), lambdaLogicalCFResourceName) template.AddResource(resourceName, lambdaPermission) return resourceName, nil }
func (roleDefinition *IAMRoleDefinition) toResource(eventSourceMappings []*EventSourceMapping, logger *logrus.Logger) gocf.IAMRole { statements := CommonIAMStatements["core"] for _, eachPrivilege := range roleDefinition.Privileges { statements = append(statements, iamPolicyStatement{ Effect: "Allow", Action: eachPrivilege.Actions, Resource: eachPrivilege.resourceExpr(), }) } // http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html for _, eachEventSourceMapping := range eventSourceMappings { arnParts := strings.Split(eachEventSourceMapping.EventSourceArn, ":") // 3rd slot is service scope if len(arnParts) >= 2 { awsService := arnParts[2] logger.Debug("Looking up common IAM privileges for EventSource: ", awsService) serviceStatements, exists := CommonIAMStatements[awsService] if exists { statements = append(statements, serviceStatements...) statements[len(statements)-1].Resource = gocf.String(eachEventSourceMapping.EventSourceArn) } } } return gocf.IAMRole{ AssumeRolePolicyDocument: AssumePolicyDocument, Policies: &gocf.IAMPoliciesList{ gocf.IAMPolicies{ PolicyDocument: ArbitraryJSONObject{ "Version": "2012-10-17", "Statement": statements, }, PolicyName: gocf.String(CloudFormationResourceName("LambdaPolicy")), }, }, } }
func corsOptionsGatewayMethod(restAPIID gocf.Stringable, resourceID gocf.Stringable) *gocf.ApiGatewayMethod { methodResponse := gocf.APIGatewayMethodMethodResponse{ StatusCode: gocf.String("200"), ResponseParameters: corsMethodResponseParams(), } integrationResponse := gocf.APIGatewayMethodIntegrationIntegrationResponse{ ResponseTemplates: map[string]string{ "application/*": "", "text/*": "", }, StatusCode: gocf.String("200"), ResponseParameters: corsIntegrationResponseParams(), } methodIntegrationIntegrationResponseList := gocf.APIGatewayMethodIntegrationIntegrationResponseList{} methodIntegrationIntegrationResponseList = append(methodIntegrationIntegrationResponseList, integrationResponse) methodResponseList := gocf.APIGatewayMethodMethodResponseList{} methodResponseList = append(methodResponseList, methodResponse) corsMethod := &gocf.ApiGatewayMethod{ HttpMethod: gocf.String("OPTIONS"), AuthorizationType: gocf.String("NONE"), RestApiId: restAPIID.String(), ResourceId: resourceID.String(), Integration: &gocf.APIGatewayMethodIntegration{ Type: gocf.String("MOCK"), RequestTemplates: map[string]string{ "application/json": "{\"statusCode\": 200}", "text/plain": "statusCode: 200", }, IntegrationResponses: &methodIntegrationIntegrationResponseList, }, MethodResponses: &methodResponseList, } return corsMethod }
// Verify & cache the IAM rolename to ARN mapping func verifyIAMRoles(ctx *workflowContext) (workflowStep, error) { // The map is either a literal Arn from a pre-existing role name // or a gocf.RefFunc() value. // Don't verify them, just create them... ctx.logger.Info("Verifying IAM Lambda execution roles") ctx.lambdaIAMRoleNameMap = make(map[string]*gocf.StringExpr, 0) svc := iam.New(ctx.awsSession) for _, eachLambda := range ctx.lambdaAWSInfos { if "" != eachLambda.RoleName && nil != eachLambda.RoleDefinition { return nil, fmt.Errorf("Both RoleName and RoleDefinition defined for lambda: %s", eachLambda.lambdaFnName) } // Get the IAM role name if "" != eachLambda.RoleName { _, exists := ctx.lambdaIAMRoleNameMap[eachLambda.RoleName] if !exists { // Check the role params := &iam.GetRoleInput{ RoleName: aws.String(eachLambda.RoleName), } ctx.logger.Debug("Checking IAM RoleName: ", eachLambda.RoleName) resp, err := svc.GetRole(params) if err != nil { ctx.logger.Error(err.Error()) return nil, err } // Cache it - we'll need it later when we create the // CloudFormation template which needs the execution Arn (not role) ctx.lambdaIAMRoleNameMap[eachLambda.RoleName] = gocf.String(*resp.Role.Arn) } } else { logicalName := eachLambda.RoleDefinition.logicalName() _, exists := ctx.lambdaIAMRoleNameMap[logicalName] if !exists { // Insert it into the resource creation map and add // the "Ref" entry to the hashmap ctx.cfTemplate.AddResource(logicalName, eachLambda.RoleDefinition.toResource(eachLambda.EventSourceMappings, ctx.logger)) ctx.lambdaIAMRoleNameMap[logicalName] = gocf.GetAtt(logicalName, "Arn") } } } ctx.logger.WithFields(logrus.Fields{ "Count": len(ctx.lambdaIAMRoleNameMap), }).Info("IAM roles verified") return createPackageStep(), nil }
func (mapping *EventSourceMapping) export(serviceName string, targetLambda *gocf.StringExpr, S3Bucket string, S3Key string, template *gocf.Template, logger *logrus.Logger) error { eventSourceMappingResource := gocf.LambdaEventSourceMapping{ EventSourceArn: gocf.String(mapping.EventSourceArn), FunctionName: targetLambda, StartingPosition: gocf.String(mapping.StartingPosition), BatchSize: gocf.Integer(mapping.BatchSize), Enabled: gocf.Bool(!mapping.Disabled), } hash := sha1.New() hash.Write([]byte(mapping.EventSourceArn)) binary.Write(hash, binary.LittleEndian, mapping.BatchSize) hash.Write([]byte(mapping.StartingPosition)) resourceName := fmt.Sprintf("LambdaES%s", hex.EncodeToString(hash.Sum(nil))) template.AddResource(resourceName, eventSourceMappingResource) return nil }
func (perm BasePermission) export(principal string, arnPrefixParts []gocf.Stringable, lambdaLogicalCFResourceName string, template *gocf.Template, S3Bucket string, S3Key string, logger *logrus.Logger) (string, error) { lambdaPermission := gocf.LambdaPermission{ Action: gocf.String("lambda:InvokeFunction"), FunctionName: gocf.GetAtt(lambdaLogicalCFResourceName, "Arn"), Principal: gocf.String(principal), } // If the Arn isn't the wildcard value, then include it. if nil != perm.SourceArn { switch perm.SourceArn.(type) { case string: // Don't be smart if the Arn value is a user supplied literal if "*" != perm.SourceArn.(string) { lambdaPermission.SourceArn = gocf.String(perm.SourceArn.(string)) } default: lambdaPermission.SourceArn = perm.sourceArnExpr(arnPrefixParts...) } } if "" != perm.SourceAccount { lambdaPermission.SourceAccount = gocf.String(perm.SourceAccount) } hash := sha1.New() hash.Write([]byte(fmt.Sprintf("%v", lambdaPermission))) resourceName := fmt.Sprintf("LambdaPerm%s", hex.EncodeToString(hash.Sum(nil))) template.AddResource(resourceName, lambdaPermission) return resourceName, nil }
func TestUnzip(t *testing.T) { resUnzip := gocf.NewResourceByType(ZipToS3Bucket) zipResource := resUnzip.(*ZipToS3BucketResource) zipResource.DestBucket = gocf.String(os.Getenv("TEST_DEST_S3_BUCKET")) zipResource.SrcBucket = gocf.String(os.Getenv("TEST_SRC_S3_BUCKET")) zipResource.SrcKeyName = gocf.String(os.Getenv("TEST_SRC_S3_KEY")) zipResource.Manifest = map[string]interface{}{ "Some": "Data", } // Put it logger := logrus.New() awsSession := awsSession(logger) createOutputs, createError := zipResource.create(awsSession, logger) if nil != createError { t.Errorf("Failed to create Unzip resource: %s", createError) } t.Logf("TestUnzip outputs: %#v", createOutputs) deleteOutputs, deleteError := zipResource.delete(awsSession, logger) if nil != deleteError { t.Errorf("Failed to create Unzip resource: %s", createError) } t.Logf("TestUnzip outputs: %#v", deleteOutputs) }
func (perm LambdaPermission) export(serviceName string, lambdaFunctionDisplayName string, lambdaLogicalCFResourceName string, template *gocf.Template, S3Bucket string, S3Key string, logger *logrus.Logger) (string, error) { return perm.BasePermission.export(gocf.String(perm.Principal), lambdaSourceArnParts, lambdaFunctionDisplayName, lambdaLogicalCFResourceName, template, S3Bucket, S3Key, logger) }
func (perm *BasePermission) sourceArnExpr(joinParts ...gocf.Stringable) *gocf.StringExpr { var parts []gocf.Stringable if nil != joinParts { parts = append(parts, joinParts...) } switch perm.SourceArn.(type) { case string: // Don't be smart if the Arn value is a user supplied literal parts = []gocf.Stringable{gocf.String(perm.SourceArn.(string))} case *gocf.StringExpr: parts = append(parts, perm.SourceArn.(*gocf.StringExpr)) case gocf.RefFunc: parts = append(parts, perm.SourceArn.(gocf.RefFunc).String()) default: panic(fmt.Sprintf("Unsupported SourceArn value type: %+v", perm.SourceArn)) } return gocf.Join("", parts...) }
func makeTemplate() *cf.Template { t := cf.NewTemplate() t.Description = "example production infrastructure" t.Parameters["DnsName"] = &cf.Parameter{ Description: "The top level DNS name for the infrastructure", Type: "String", Default: "preview.example.io", } t.AddResource("ServerLoadBalancer", cf.ElasticLoadBalancingLoadBalancer{ ConnectionDrainingPolicy: &cf.ElasticLoadBalancingConnectionDrainingPolicy{ Enabled: cf.Bool(true), Timeout: cf.Integer(30), }, CrossZone: cf.Bool(true), HealthCheck: &cf.ElasticLoadBalancingHealthCheck{ HealthyThreshold: cf.String("2"), Interval: cf.String("60"), Target: cf.String("HTTP:80/"), Timeout: cf.String("5"), UnhealthyThreshold: cf.String("2"), }, Listeners: &cf.ElasticLoadBalancingListenerList{ cf.ElasticLoadBalancingListener{ InstancePort: cf.String("8000"), InstanceProtocol: cf.String("TCP"), LoadBalancerPort: cf.String("443"), Protocol: cf.String("SSL"), SSLCertificateId: cf.Join("", cf.String("arn:aws:iam::"), cf.Ref("AWS::AccountID"), cf.String(":server-certificate/"), cf.Ref("DnsName")), }, }, Policies: &cf.ElasticLoadBalancingPolicyList{ cf.ElasticLoadBalancingPolicy{ PolicyName: cf.String("EnableProxyProtocol"), PolicyType: cf.String("ProxyProtocolPolicyType"), Attributes: []map[string]interface{}{ map[string]interface{}{ "Name": "ProxyProtocol", "Value": "true", }, }, InstancePorts: []int{8000}, }, }, Subnets: cf.StringList( cf.Ref("VpcSubnetA"), cf.Ref("VpcSubnetB"), cf.Ref("VpcSubnetC"), ), SecurityGroups: cf.StringList(cf.Ref("LoadBalancerSecurityGroup")), }) return t }
// Marshal this object into 1 or more CloudFormation resource definitions that are accumulated // in the resources map func (info *LambdaAWSInfo) export(serviceName string, S3Bucket string, S3Key string, buildID string, roleNameMap map[string]*gocf.StringExpr, template *gocf.Template, context map[string]interface{}, logger *logrus.Logger) error { // If we have RoleName, then get the ARN, otherwise get the Ref var dependsOn []string if nil != info.DependsOn { dependsOn = append(dependsOn, info.DependsOn...) } iamRoleArnName := info.RoleName // If there is no user supplied role, that means that the associated // IAMRoleDefinition name has been created and this resource needs to // depend on that being created. if iamRoleArnName == "" && info.RoleDefinition != nil { iamRoleArnName = info.RoleDefinition.logicalName(serviceName, info.lambdaFunctionName()) dependsOn = append(dependsOn, info.RoleDefinition.logicalName(serviceName, info.lambdaFunctionName())) } lambdaDescription := info.Options.Description if "" == lambdaDescription { lambdaDescription = fmt.Sprintf("%s: %s", serviceName, info.lambdaFunctionName()) } // Create the primary resource lambdaResource := gocf.LambdaFunction{ Code: &gocf.LambdaFunctionCode{ S3Bucket: gocf.String(S3Bucket), S3Key: gocf.String(S3Key), }, Description: gocf.String(lambdaDescription), Handler: gocf.String(fmt.Sprintf("index.%s", info.jsHandlerName())), MemorySize: gocf.Integer(info.Options.MemorySize), Role: roleNameMap[iamRoleArnName], Runtime: gocf.String(NodeJSVersion), Timeout: gocf.Integer(info.Options.Timeout), VpcConfig: info.Options.VpcConfig, } if "" != info.Options.KmsKeyArn { lambdaResource.KmsKeyArn = gocf.String(info.Options.KmsKeyArn) } if nil != info.Options.Environment { lambdaResource.Environment = &gocf.LambdaFunctionEnvironment{ Variables: info.Options.Environment, } } // Need to check if a functionName exists in the LambdaAwsInfo struct // If an empty string is passed, the template will error with invalid // function name. if "" != info.functionName { lambdaResource.FunctionName = gocf.String(info.functionName) } cfResource := template.AddResource(info.logicalName(), lambdaResource) cfResource.DependsOn = append(cfResource.DependsOn, dependsOn...) safeMetadataInsert(cfResource, "golangFunc", info.lambdaFunctionName()) // Create the lambda Ref in case we need a permission or event mapping functionAttr := gocf.GetAtt(info.logicalName(), "Arn") // Permissions for _, eachPermission := range info.Permissions { _, err := eachPermission.export(serviceName, info.lambdaFunctionName(), info.logicalName(), template, S3Bucket, S3Key, logger) if nil != err { return err } } // Event Source Mappings for _, eachEventSourceMapping := range info.EventSourceMappings { mappingErr := eachEventSourceMapping.export(serviceName, functionAttr, S3Bucket, S3Key, template, logger) if nil != mappingErr { return mappingErr } } // CustomResource for _, eachCustomResource := range info.customResources { resourceErr := eachCustomResource.export(serviceName, functionAttr, S3Bucket, S3Key, roleNameMap, template, logger) if nil != resourceErr { return resourceErr } } // Decorator if nil != info.Decorator { logger.Debug("Decorator found for Lambda: ", info.lambdaFunctionName()) // Create an empty template so that we can track whether things // are overwritten metadataMap := make(map[string]interface{}, 0) decoratorProxyTemplate := gocf.NewTemplate() err := info.Decorator(serviceName, info.logicalName(), lambdaResource, metadataMap, S3Bucket, S3Key, buildID, decoratorProxyTemplate, context, logger) if nil != err { return err } // This data is marshalled into a DiscoveryInfo struct s.t. it can be // unmarshalled via sparta.Discover. We're going to just stuff it into // it's own same named property if len(metadataMap) != 0 { safeMetadataInsert(cfResource, info.logicalName(), metadataMap) } // Append the custom resources err = safeMergeTemplates(decoratorProxyTemplate, template, logger) if nil != err { return fmt.Errorf("Lambda (%s) decorator created conflicting resources", info.lambdaFunctionName()) } } return nil }
func (perm CloudWatchEventsPermission) export(serviceName string, lambdaLogicalCFResourceName string, template *gocf.Template, S3Bucket string, S3Key string, logger *logrus.Logger) (string, error) { // Tell the user we're ignoring any Arns provided, since it doesn't make sense for // this. if nil != perm.BasePermission.SourceArn && perm.BasePermission.sourceArnExpr(cloudformationEventsSourceArnParts...).String() != wildcardArn.String() { logger.WithFields(logrus.Fields{ "Arn": perm.BasePermission.sourceArnExpr(cloudformationEventsSourceArnParts...), }).Warn("CloudWatchEvents do not support literal ARN values") } arnPermissionForRuleName := func(ruleName string) *gocf.StringExpr { return gocf.Join("", gocf.String("arn:aws:events:"), gocf.Ref("AWS::Region"), gocf.String(":"), gocf.Ref("AWS::AccountId"), gocf.String(":rule/"), gocf.String(ruleName)) } // First thing we need to do is uniqueify the rule names s.t. we prevent // collisions with other stacks. globallyUniqueRules := make(map[string]CloudWatchEventsRule, len(perm.Rules)) for eachRuleName, eachDefinition := range perm.Rules { uniqueRuleName := CloudFormationResourceName(eachRuleName, lambdaLogicalCFResourceName, serviceName) // Trim it... if len(eachDefinition.Description) <= 0 { eachDefinition.Description = fmt.Sprintf("%s CloudWatch Events rule for service: %s", eachRuleName, serviceName) } globallyUniqueRules[uniqueRuleName] = eachDefinition } // Self test - there should only be 1 element since we're only ever configuring // the same AWS principal service. If we end up with multiple configuration resource names // it means that the stable resource name logic is broken configurationResourceNames := make(map[string]int, 0) var dependsOn []string for eachRuleName := range globallyUniqueRules { basePerm := BasePermission{ SourceArn: arnPermissionForRuleName(eachRuleName), } dependOn, err := basePerm.export(CloudWatchEventsPrincipal, cloudformationEventsSourceArnParts, lambdaLogicalCFResourceName, template, S3Bucket, S3Key, logger) if nil != err { return "", err } dependsOn = append(dependsOn, dependOn) // Ensure the configurator for this ARNs sourceArnExpression := basePerm.sourceArnExpr(cloudformationEventsSourceArnParts...) // Make sure the custom lambda that manages CloudWatch Events is provisioned. configuratorResName, err := ensureConfiguratorLambdaResource(CloudWatchEventsPrincipal, sourceArnExpression, []string{}, template, S3Bucket, S3Key, logger) if nil != err { return "", err } configurationResourceNames[configuratorResName] = 1 } // Although we ensured multiple configuration resources, they were all for the // same AWS principal. We're only supposed to get a single name back. if len(configurationResourceNames) > 1 { return "", fmt.Errorf("Multiple configuration resources detected: %#v", configurationResourceNames) } else if len(configurationResourceNames) == 0 { return "", fmt.Errorf("CloudWatchEvent configuration provider failed") } // Insert the invocation for eachConfigResource := range configurationResourceNames { ////////////////////////////////////////////////////////////////////////////// // And finally the custom resource forwarder newResource, err := newCloudFormationResource("Custom::SpartaCloudWatchEventsPermission", logger) if nil != err { return "", err } customResource := newResource.(*cloudformationCloudWatchEventsPermissionResource) customResource.ServiceToken = gocf.GetAtt(eachConfigResource, "Arn") customResource.Rules = globallyUniqueRules customResource.LambdaTarget = gocf.GetAtt(lambdaLogicalCFResourceName, "Arn") // Name? resourceInvokerName := CloudFormationResourceName("ConfigCloudWatchEvents", lambdaLogicalCFResourceName, perm.BasePermission.SourceAccount) // Add it cfResource := template.AddResource(resourceInvokerName, customResource) cfResource.DependsOn = append(cfResource.DependsOn, dependsOn...) } return "", nil }
func (converter *templateConverter) parseData() *templateConverter { if converter.conversionError != nil { return converter } reAWSProp := regexp.MustCompile("\\{\\s*\"\\s*(Ref|Fn::GetAtt|Fn::FindInMap)") splitData := strings.Split(converter.expandedTemplate, "\n") splitDataLineCount := len(splitData) for eachLineIndex, eachLine := range splitData { curContents := eachLine for len(curContents) != 0 { matchInfo := reAWSProp.FindStringSubmatchIndex(curContents) if nil != matchInfo { // If there's anything at the head, push it. if matchInfo[0] != 0 { head := curContents[0:matchInfo[0]] converter.contents = append(converter.contents, gocf.String(fmt.Sprintf("%s", head))) curContents = curContents[len(head):] } // There's at least one match...find the closing brace... var parsed map[string]interface{} for indexPos, eachChar := range curContents { if string(eachChar) == "}" { testBlock := curContents[0 : indexPos+1] err := json.Unmarshal([]byte(testBlock), &parsed) if err == nil { parsedContents, parsedContentsErr := parseFnJoinExpr(parsed) if nil != parsedContentsErr { converter.conversionError = parsedContentsErr return converter } converter.contents = append(converter.contents, parsedContents) curContents = curContents[indexPos+1:] if len(curContents) <= 0 && (eachLineIndex < (splitDataLineCount - 1)) { converter.contents = append(converter.contents, gocf.String("\n")) } break } } } if nil == parsed { // We never did find the end... converter.conversionError = fmt.Errorf("Invalid CloudFormation JSON expression on line: %s", eachLine) return converter } } else { // No match, just include it iff there is another line afterwards newlineValue := "" if eachLineIndex < (splitDataLineCount - 1) { newlineValue = "\n" } // Always include a newline at a minimum appendLine := fmt.Sprintf("%s%s", curContents, newlineValue) if len(appendLine) != 0 { converter.contents = append(converter.contents, gocf.String(appendLine)) } break } } } return converter }
return nil } } func init() { gocf.RegisterCustomResourceProvider(customResourceProvider) rand.Seed(time.Now().Unix()) } //////////////////////////////////////////////////////////////////////////////// // Variables //////////////////////////////////////////////////////////////////////////////// // Represents the CloudFormation Arn of this stack, referenced // in CommonIAMStatements var cloudFormationThisStackArn = []gocf.Stringable{gocf.String("arn:aws:cloudformation:"), gocf.Ref("AWS::Region").String(), gocf.String(":"), gocf.Ref("AWS::AccountId").String(), gocf.String(":stack/"), gocf.Ref("AWS::StackName").String(), gocf.String("/*")} // CommonIAMStatements defines common IAM::Role Policy Statement values for different AWS // service types. See http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#genref-aws-service-namespaces // for names. // http://docs.aws.amazon.com/lambda/latest/dg/monitoring-functions.html // for more information. var CommonIAMStatements = struct { Core []spartaIAM.PolicyStatement VPC []spartaIAM.PolicyStatement
// export marshals the API data to a CloudFormation compatible representation func (api *API) export(serviceName string, session *session.Session, S3Bucket string, S3Key string, roleNameMap map[string]*gocf.StringExpr, template *gocf.Template, noop bool, logger *logrus.Logger) error { apiGatewayResourceNameForPath := func(fullPath string) string { pathParts := strings.Split(fullPath, "/") return CloudFormationResourceName("%sResource", pathParts[0], fullPath) } apiGatewayResName := CloudFormationResourceName("APIGateway", api.name) // Create an API gateway entry apiGatewayRes := &gocf.ApiGatewayRestApi{ Description: gocf.String(api.Description), FailOnWarnings: gocf.Bool(false), Name: gocf.String(api.name), } if "" != api.CloneFrom { apiGatewayRes.CloneFrom = gocf.String(api.CloneFrom) } if "" == api.Description { apiGatewayRes.Description = gocf.String(fmt.Sprintf("%s RestApi", serviceName)) } else { apiGatewayRes.Description = gocf.String(api.Description) } template.AddResource(apiGatewayResName, apiGatewayRes) apiGatewayRestAPIID := gocf.Ref(apiGatewayResName) // List of all the method resources we're creating s.t. the // deployment can DependOn them optionsMethodPathMap := make(map[string]bool) var apiMethodCloudFormationResources []string for eachResourceMethodKey, eachResourceDef := range api.resources { // First walk all the user resources and create intermediate paths // to repreesent all the resources var parentResource *gocf.StringExpr pathParts := strings.Split(strings.TrimLeft(eachResourceDef.pathPart, "/"), "/") pathAccumulator := []string{"/"} for index, eachPathPart := range pathParts { pathAccumulator = append(pathAccumulator, eachPathPart) resourcePathName := apiGatewayResourceNameForPath(strings.Join(pathAccumulator, "/")) if _, exists := template.Resources[resourcePathName]; !exists { cfResource := &gocf.ApiGatewayResource{ RestApiId: apiGatewayRestAPIID.String(), PathPart: gocf.String(eachPathPart), } if index <= 0 { cfResource.ParentId = gocf.GetAtt(apiGatewayResName, "RootResourceId") } else { cfResource.ParentId = parentResource } template.AddResource(resourcePathName, cfResource) } parentResource = gocf.Ref(resourcePathName).String() } // Add the lambda permission apiGatewayPermissionResourceName := CloudFormationResourceName("APIGatewayLambdaPerm", eachResourceMethodKey) lambdaInvokePermission := &gocf.LambdaPermission{ Action: gocf.String("lambda:InvokeFunction"), FunctionName: gocf.GetAtt(eachResourceDef.parentLambda.logicalName(), "Arn"), Principal: gocf.String(APIGatewayPrincipal), } template.AddResource(apiGatewayPermissionResourceName, lambdaInvokePermission) // BEGIN CORS - OPTIONS verb // CORS is API global, but it's possible that there are multiple different lambda functions // that are handling the same HTTP resource. In this case, track whether we've already created an // OPTIONS entry for this path and only append iff this is the first time through if api.CORSEnabled { methodResourceName := CloudFormationResourceName(fmt.Sprintf("%s-OPTIONS", eachResourceDef.pathPart), eachResourceDef.pathPart) _, resourceExists := optionsMethodPathMap[methodResourceName] if !resourceExists { template.AddResource(methodResourceName, corsOptionsGatewayMethod(apiGatewayRestAPIID, parentResource)) apiMethodCloudFormationResources = append(apiMethodCloudFormationResources, methodResourceName) optionsMethodPathMap[methodResourceName] = true } } // END CORS - OPTIONS verb // BEGIN - user defined verbs for eachMethodName, eachMethodDef := range eachResourceDef.Methods { apiGatewayMethod := &gocf.ApiGatewayMethod{ HttpMethod: gocf.String(eachMethodName), AuthorizationType: gocf.String("NONE"), ResourceId: parentResource.String(), RestApiId: apiGatewayRestAPIID.String(), Integration: &gocf.APIGatewayMethodIntegration{ IntegrationHttpMethod: gocf.String("POST"), Type: gocf.String("AWS"), RequestTemplates: defaultRequestTemplates(), Uri: gocf.Join("", gocf.String("arn:aws:apigateway:"), gocf.Ref("AWS::Region"), gocf.String(":lambda:path/2015-03-31/functions/"), gocf.GetAtt(eachResourceDef.parentLambda.logicalName(), "Arn"), gocf.String("/invocations")), }, } if len(eachMethodDef.Parameters) != 0 { requestParams := make(map[string]string, 0) for eachKey, eachBool := range eachMethodDef.Parameters { requestParams[eachKey] = fmt.Sprintf("%t", eachBool) } apiGatewayMethod.RequestParameters = requestParams } // Add the integration response RegExps apiGatewayMethod.Integration.IntegrationResponses = integrationResponses(eachMethodDef.Integration.Responses, api.CORSEnabled) // Add outbound method responses apiGatewayMethod.MethodResponses = methodResponses(eachMethodDef.Responses, api.CORSEnabled) prefix := fmt.Sprintf("%s%s", eachMethodDef.httpMethod, eachResourceMethodKey) methodResourceName := CloudFormationResourceName(prefix, eachResourceMethodKey, serviceName) res := template.AddResource(methodResourceName, apiGatewayMethod) res.DependsOn = append(res.DependsOn, apiGatewayPermissionResourceName) apiMethodCloudFormationResources = append(apiMethodCloudFormationResources, methodResourceName) } } // END if nil != api.stage { // Is the stack already deployed? stageName := api.stage.name stageInfo, stageInfoErr := apiStageInfo(api.name, stageName, session, noop, logger) if nil != stageInfoErr { return stageInfoErr } if nil == stageInfo { // Use a stable identifier so that we can update the existing deployment apiDeploymentResName := CloudFormationResourceName("APIGatewayDeployment", serviceName) apiDeployment := &gocf.ApiGatewayDeployment{ Description: gocf.String(api.stage.Description), RestApiId: apiGatewayRestAPIID.String(), StageName: gocf.String(stageName), StageDescription: &gocf.APIGatewayDeploymentStageDescription{ StageName: gocf.String(api.stage.name), Description: gocf.String(api.stage.Description), Variables: api.stage.Variables, }, } if api.stage.CacheClusterEnabled { apiDeployment.StageDescription.CacheClusterEnabled = gocf.Bool(api.stage.CacheClusterEnabled) } if api.stage.CacheClusterSize != "" { apiDeployment.StageDescription.CacheClusterSize = gocf.String(api.stage.CacheClusterSize) } deployment := template.AddResource(apiDeploymentResName, apiDeployment) deployment.DependsOn = append(deployment.DependsOn, apiMethodCloudFormationResources...) deployment.DependsOn = append(deployment.DependsOn, apiGatewayResName) } else { newDeployment := &gocf.ApiGatewayDeployment{ Description: gocf.String("Sparta deploy"), RestApiId: apiGatewayRestAPIID.String(), StageName: gocf.String(stageName), } // Use an unstable ID s.t. we can actually create a new deployment event. Not sure how this // is going to work with deletes... deploymentResName := CloudFormationResourceName("APIGatewayDeployment") deployment := template.AddResource(deploymentResName, newDeployment) deployment.DependsOn = append(deployment.DependsOn, apiMethodCloudFormationResources...) deployment.DependsOn = append(deployment.DependsOn, apiGatewayResName) } template.Outputs[OutputAPIGatewayURL] = &gocf.Output{ Description: "API Gateway URL", Value: gocf.Join("", gocf.String("https://"), apiGatewayRestAPIID, gocf.String(".execute-api."), gocf.Ref("AWS::Region"), gocf.String(".amazonaws.com/"), gocf.String(stageName)), } } return nil }
// ensureIAMRoleForCustomResource ensures that the single IAM::Role for a single // AWS principal (eg, s3.*.*) exists, and includes statements for the given // sourceArn. Sparta uses a single IAM::Role for the CustomResource configuration // lambda, which is the union of all Arns in the application. func ensureIAMRoleForCustomResource(awsPrincipalName string, sourceArn *gocf.StringExpr, template *gocf.Template, logger *logrus.Logger) (string, error) { var principalActions []string switch awsPrincipalName { case cloudformationresources.SNSLambdaEventSource: principalActions = PushSourceConfigurationActions.SNSLambdaEventSource case cloudformationresources.S3LambdaEventSource: principalActions = PushSourceConfigurationActions.S3LambdaEventSource case cloudformationresources.SESLambdaEventSource: principalActions = PushSourceConfigurationActions.SESLambdaEventSource case cloudformationresources.CloudWatchLogsLambdaEventSource: principalActions = PushSourceConfigurationActions.CloudWatchLogsLambdaEventSource default: return "", fmt.Errorf("Unsupported principal for IAM role creation: %s", awsPrincipalName) } // What's the stable IAMRoleName? resourceBaseName := fmt.Sprintf("CustomResource%sIAMRole", awsPrincipalToService(awsPrincipalName)) stableRoleName := CloudFormationResourceName(resourceBaseName, awsPrincipalName) // Ensure it exists, then check to see if this Source ARN is already specified... // Checking equality with Stringable? // Create a new Role var existingIAMRole *gocf.IAMRole existingResource, exists := template.Resources[stableRoleName] logger.WithFields(logrus.Fields{ "PrincipalActions": principalActions, "SourceArn": sourceArn, }).Debug("Ensuring IAM Role results") if !exists { // Insert the IAM role here. We'll walk the policies data in the next section // to make sure that the sourceARN we have is in the list statements := CommonIAMStatements.Core iamPolicyList := gocf.IAMPoliciesList{} iamPolicyList = append(iamPolicyList, gocf.IAMPolicies{ PolicyDocument: ArbitraryJSONObject{ "Version": "2012-10-17", "Statement": statements, }, PolicyName: gocf.String(fmt.Sprintf("%sPolicy", stableRoleName)), }, ) existingIAMRole = &gocf.IAMRole{ AssumeRolePolicyDocument: AssumePolicyDocument, Policies: &iamPolicyList, } template.AddResource(stableRoleName, existingIAMRole) // Create a new IAM Role resource logger.WithFields(logrus.Fields{ "RoleName": stableRoleName, }).Debug("Inserting IAM Role") } else { existingIAMRole = existingResource.Properties.(*gocf.IAMRole) } // Walk the existing statements if nil != existingIAMRole.Policies { for _, eachPolicy := range *existingIAMRole.Policies { policyDoc := eachPolicy.PolicyDocument.(ArbitraryJSONObject) statements := policyDoc["Statement"] for _, eachStatement := range statements.([]spartaIAM.PolicyStatement) { if sourceArn.String() == eachStatement.Resource.String() { logger.WithFields(logrus.Fields{ "RoleName": stableRoleName, "SourceArn": sourceArn.String(), }).Debug("SourceArn already exists for IAM Policy") return stableRoleName, nil } } } logger.WithFields(logrus.Fields{ "RoleName": stableRoleName, "Action": principalActions, "Resource": sourceArn, }).Debug("Inserting Actions for configuration ARN") // Add this statement to the first policy, iff the actions are non-empty if len(principalActions) > 0 { rootPolicy := (*existingIAMRole.Policies)[0] rootPolicyDoc := rootPolicy.PolicyDocument.(ArbitraryJSONObject) rootPolicyStatements := rootPolicyDoc["Statement"].([]spartaIAM.PolicyStatement) rootPolicyDoc["Statement"] = append(rootPolicyStatements, spartaIAM.PolicyStatement{ Effect: "Allow", Action: principalActions, Resource: sourceArn, }) } return stableRoleName, nil } return "", fmt.Errorf("Unable to find Policies entry for IAM role: %s", stableRoleName) }
func ensureCustomResourceHandler(serviceName string, customResourceTypeName string, sourceArn *gocf.StringExpr, dependsOn []string, template *gocf.Template, S3Bucket string, S3Key string, logger *logrus.Logger) (string, error) { // AWS service basename awsServiceName := awsPrincipalToService(customResourceTypeName) // Use a stable resource CloudFormation resource name to represent // the single CustomResource that can configure the different // PushSource's for the given principal. keyName, err := json.Marshal(ArbitraryJSONObject{ "Principal": customResourceTypeName, "ServiceName": awsServiceName, }) if err != nil { logger.Error("Failed to create configurator resource name: ", err.Error()) return "", err } subscriberHandlerName := CloudFormationResourceName(fmt.Sprintf("%sCustomResource", awsServiceName), string(keyName)) ////////////////////////////////////////////////////////////////////////////// // IAM Role definition iamResourceName, err := ensureIAMRoleForCustomResource(customResourceTypeName, sourceArn, template, logger) if nil != err { return "", err } iamRoleRef := gocf.GetAtt(iamResourceName, "Arn") _, exists := template.Resources[subscriberHandlerName] if !exists { logger.WithFields(logrus.Fields{ "Service": customResourceTypeName, }).Debug("Including Lambda CustomResource for AWS Service") configuratorDescription := customResourceDescription(serviceName, customResourceTypeName) ////////////////////////////////////////////////////////////////////////////// // Custom Resource Lambda Handler // The export name MUST correspond to the createForwarder entry that is dynamically // written into the index.js file during compile in createNewSpartaCustomResourceEntry handlerName := lambdaExportNameForCustomResourceType(customResourceTypeName) logger.WithFields(logrus.Fields{ "CustomResourceType": customResourceTypeName, "NodeJSExport": handlerName, }).Debug("Sparta CloudFormation custom resource handler info") customResourceHandlerDef := gocf.LambdaFunction{ Code: &gocf.LambdaFunctionCode{ S3Bucket: gocf.String(S3Bucket), S3Key: gocf.String(S3Key), }, Description: gocf.String(configuratorDescription), Handler: gocf.String(handlerName), Role: iamRoleRef, Runtime: gocf.String(NodeJSVersion), Timeout: gocf.Integer(30), } cfResource := template.AddResource(subscriberHandlerName, customResourceHandlerDef) if nil != dependsOn && (len(dependsOn) > 0) { cfResource.DependsOn = append(cfResource.DependsOn, dependsOn...) } } return subscriberHandlerName, nil }