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 (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 (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 (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 (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 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 }
// 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) }
// AddAutoIncrementingLambdaVersionResource inserts a new // AWS::Lambda::Version resource into the template. It uses // the existing CloudFormation template representation // to determine the version index to append. The returned // map is from `versionIndex`->`CloudFormationResourceName` // to support second-order AWS::Lambda::Alias records on a // per-version level func AddAutoIncrementingLambdaVersionResource(serviceName string, lambdaResourceName string, cfTemplate *gocf.Template, logger *logrus.Logger) (*AutoIncrementingLambdaVersionInfo, error) { // Get the template session, sessionErr := session.NewSession() if sessionErr != nil { return nil, sessionErr } // Get the current template - for each version we find in the version listing // we look up the actual CF resource and copy it into this template existingStackDefinition, existingStackDefinitionErr := existingStackTemplate(serviceName, session, logger) if nil != existingStackDefinitionErr { return nil, existingStackDefinitionErr } existingVersions, existingVersionsErr := existingLambdaResourceVersions(serviceName, lambdaResourceName, session, logger) if nil != existingVersionsErr { return nil, existingVersionsErr } // Initialize the auto incrementing version struct autoIncrementingLambdaVersionInfo := AutoIncrementingLambdaVersionInfo{ CurrentVersion: 0, CurrentVersionResourceName: "", VersionHistory: make(map[int]string, 0), } lambdaVersionResourceName := func(versionIndex int) string { return CloudFormationResourceName(lambdaResourceName, "version", strconv.Itoa(versionIndex)) } if nil != existingVersions { // Add the CloudFormation resource logger.WithFields(logrus.Fields{ "VersionCount": len(existingVersions.Versions) - 1, // Ignore $LATEST "ResourceName": lambdaResourceName, }).Info("Total number of published versions") for _, eachEntry := range existingVersions.Versions { versionIndex, versionIndexErr := strconv.Atoi(*eachEntry.Version) if nil == versionIndexErr { // Find the existing resource... versionResourceName := lambdaVersionResourceName(versionIndex) if nil == existingStackDefinition { return nil, fmt.Errorf("Unable to find exising Version resource in nil Template") } cfResourceDefinition, cfResourceDefinitionExists := existingStackDefinition.Resources[versionResourceName] if !cfResourceDefinitionExists { return nil, fmt.Errorf("Unable to find exising Version resource (Resource: %s, Version: %d) in template", versionResourceName, versionIndex) } cfTemplate.Resources[versionResourceName] = cfResourceDefinition // Add the CloudFormation resource logger.WithFields(logrus.Fields{ "Version": versionIndex, "ResourceName": versionResourceName, }).Debug("Preserving Lambda version") // Store the state, tracking the latest version autoIncrementingLambdaVersionInfo.VersionHistory[versionIndex] = versionResourceName if versionIndex > autoIncrementingLambdaVersionInfo.CurrentVersion { autoIncrementingLambdaVersionInfo.CurrentVersion = versionIndex } } } } // Bump the version and add a new entry... autoIncrementingLambdaVersionInfo.CurrentVersion++ versionResource := &gocf.LambdaVersion{ FunctionName: gocf.GetAtt(lambdaResourceName, "Arn").String(), } autoIncrementingLambdaVersionInfo.CurrentVersionResourceName = lambdaVersionResourceName(autoIncrementingLambdaVersionInfo.CurrentVersion) cfTemplate.AddResource(autoIncrementingLambdaVersionInfo.CurrentVersionResourceName, versionResource) // Log the version we're about to publish... logger.WithFields(logrus.Fields{ "ResourceName": lambdaResourceName, "StackVersion": autoIncrementingLambdaVersionInfo.CurrentVersion, }).Info("Inserting new version resource") return &autoIncrementingLambdaVersionInfo, nil }
func (perm SESPermission) export(serviceName 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(SESPrincipal, sesSourcePartArn, lambdaLogicalCFResourceName, template, S3Bucket, S3Key, logger) if nil != err { return "", err } // MessageBody storage? var dependsOn []string if nil != perm.MessageBodyStorage { s3Policy, err := perm.MessageBodyStorage.export(serviceName, lambdaLogicalCFResourceName, template, S3Bucket, S3Key, logger) if nil != err { return "", err } if "" != s3Policy { dependsOn = append(dependsOn, s3Policy) } } // Make sure the custom lambda that manages SNS notifications is provisioned. configuratorResName, err := ensureConfiguratorLambdaResource(SESPrincipal, sourceArnExpression, dependsOn, template, S3Bucket, S3Key, logger) if nil != err { return "", err } // Add a custom resource invocation for this configuration invocationType := perm.InvocationType if "" == invocationType { invocationType = "Event" } // If there aren't any, just forward everything receiptRules := perm.ReceiptRules if nil == perm.ReceiptRules { receiptRules = []ReceiptRule{ReceiptRule{ Name: "Default", Disabled: false, ScanDisabled: false, Recipients: []string{}, TLSPolicy: "Optional", }} } var xformedRules []ArbitraryJSONObject for _, eachReceiptRule := range receiptRules { xformedRules = append(xformedRules, eachReceiptRule.lambdaTargetReceiptRule( serviceName, gocf.GetAtt(lambdaLogicalCFResourceName, "Arn"), perm.MessageBodyStorage)) } newResource, err := newCloudFormationResource("Custom::SpartaSESPermission", logger) if nil != err { return "", err } customResource := newResource.(*cloudFormationSESPermissionResource) customResource.ServiceToken = gocf.GetAtt(configuratorResName, "Arn") customResource.Rules = xformedRules subscriberResourceName := CloudFormationResourceName("SubscriberSES", targetLambdaResourceName, perm.BasePermission.SourceAccount, fmt.Sprintf("%v", perm.BasePermission.SourceArn)) cfResource := template.AddResource(subscriberResourceName, customResource) cfResource.DependsOn = append(cfResource.DependsOn, targetLambdaResourceName, configuratorResName) 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 }
// export marshals the API data to a CloudFormation compatible representation func (api *API) export(S3Bucket string, S3Key string, roleNameMap map[string]*gocf.StringExpr, template *gocf.Template, logger *logrus.Logger) error { lambdaResourceName, err := ensureConfiguratorLambdaResource(APIGatewayPrincipal, gocf.String("*"), []string{}, template, S3Bucket, S3Key, logger) if nil != err { return err } // If this API is CORS enabled, then annotate the APIResources with OPTION // entries. Slight overhead in network I/O due to marshalling data, but simplifies // the CustomResource, which is only a temporary stopgap until cloudformation // properly supports APIGateway responseParameters := map[string]bool{ "method.response.header.Access-Control-Allow-Headers": true, "method.response.header.Access-Control-Allow-Methods": true, "method.response.header.Access-Control-Allow-Origin": true, } integrationResponseParameters := map[string]string{ "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key'", "method.response.header.Access-Control-Allow-Methods": "'*'", "method.response.header.Access-Control-Allow-Origin": "'*'", } // Keep track of how many resources && methods we're supposed to provision. If there // aren't any, then throw an error resourceCount := 0 methodCount := 0 // We need to update the default values here, because the individual // methods are deserialized they annotate the prexisting responses with whitelist data. for _, eachResource := range api.resources { resourceCount++ if api.CORSEnabled { // Create the OPTIONS entry method, err := eachResource.NewMethod("OPTIONS") if err != nil { return err } methodCount++ statusOkResponse := defaultResponse() statusOkResponse.Parameters = responseParameters method.Responses[200] = statusOkResponse method.Integration = Integration{ Parameters: make(map[string]string, 0), RequestTemplates: make(map[string]string, 0), Responses: make(map[int]*IntegrationResponse, 0), integrationType: "MOCK", } method.Integration.RequestTemplates["application/json"] = "{\"statusCode\": 200}" corsIntegrationResponse := IntegrationResponse{ Parameters: integrationResponseParameters, Templates: map[string]string{ "application/json": "", }, } method.Integration.Responses[200] = &corsIntegrationResponse } for _, eachMethod := range eachResource.Methods { methodCount++ statusSuccessfulCode := http.StatusOK if eachMethod.httpMethod == "POST" { statusSuccessfulCode = http.StatusCreated } if len(eachMethod.Responses) <= 0 { eachMethod.Responses = DefaultMethodResponses(statusSuccessfulCode) } if api.CORSEnabled { for _, eachResponse := range eachMethod.Responses { if nil == eachResponse.Parameters { eachResponse.Parameters = make(map[string]bool, 0) } for eachKey, eachBool := range responseParameters { eachResponse.Parameters[eachKey] = eachBool } } } // Update Integration if len(eachMethod.Integration.Responses) <= 0 { eachMethod.Integration.Responses = DefaultIntegrationResponses(statusSuccessfulCode) } if api.CORSEnabled { for eachHTTPStatus, eachIntegrationResponse := range eachMethod.Integration.Responses { if eachHTTPStatus >= 200 && eachHTTPStatus <= 299 { if nil == eachIntegrationResponse.Parameters { eachIntegrationResponse.Parameters = make(map[string]string, 0) } for eachKey, eachValue := range integrationResponseParameters { eachIntegrationResponse.Parameters[eachKey] = eachValue } } } } } } if resourceCount <= 0 || methodCount <= 0 { logger.WithFields(logrus.Fields{ "ResourceCount": resourceCount, "MethodCount": methodCount, }).Error("*sparta.API value provided to sparta.Main(), but no resources or methods were defined") return errors.New("Non-nil, empty *sparta.API provided to sparta.Main(). Prefer `nil` value") } // Unmarshal everything to JSON newResource, err := newCloudFormationResource("Custom::SpartaAPIGateway", logger) if nil != err { return err } apiGatewayResource := newResource.(*cloudFormationAPIGatewayResource) apiGatewayResource.ServiceToken = gocf.GetAtt(lambdaResourceName, "Arn") apiGatewayResource.API = api apiGatewayInvokerResName := CloudFormationResourceName("APIGateway", api.name) cfResource := template.AddResource(apiGatewayInvokerResName, apiGatewayResource) cfResource.DependsOn = append(cfResource.DependsOn, lambdaResourceName) // Save the output template.Outputs[OutputAPIGatewayURL] = &gocf.Output{ Description: "API Gateway URL", Value: gocf.GetAtt(apiGatewayInvokerResName, "URL"), } return nil }
func (storage *MessageBodyStorage) export(serviceName string, lambdaLogicalCFResourceName string, template *gocf.Template, S3Bucket string, S3Key string, logger *logrus.Logger) (string, error) { if "" != storage.cloudFormationS3BucketResourceName { s3Bucket := &gocf.S3Bucket{ Tags: []gocf.ResourceTag{ gocf.ResourceTag{ Key: gocf.String("sparta:logicalBucketName"), Value: gocf.String(storage.logicalBucketName), }, }, } cfResource := template.AddResource(storage.cloudFormationS3BucketResourceName, s3Bucket) cfResource.DeletionPolicy = "Retain" lambdaResource, _ := template.Resources[lambdaLogicalCFResourceName] if nil != lambdaResource { safeAppendDependency(lambdaResource, storage.cloudFormationS3BucketResourceName) } logger.WithFields(logrus.Fields{ "LogicalResourceName": storage.cloudFormationS3BucketResourceName, }).Info("Service will orphan S3 Bucket on deletion") // Save the output template.Outputs[storage.cloudFormationS3BucketResourceName] = &gocf.Output{ Description: "SES Message Body Bucket", Value: gocf.Ref(storage.cloudFormationS3BucketResourceName), } } // Add the S3 Access policy s3BodyStoragePolicy := &gocf.S3BucketPolicy{ Bucket: storage.bucketNameExpr, PolicyDocument: ArbitraryJSONObject{ "Version": "2012-10-17", "Statement": []ArbitraryJSONObject{ { "Sid": "PermitSESServiceToSaveEmailBody", "Effect": "Allow", "Principal": ArbitraryJSONObject{ "Service": "ses.amazonaws.com", }, "Action": []string{"s3:PutObjectAcl", "s3:PutObject"}, "Resource": gocf.Join("", gocf.String("arn:aws:s3:::"), storage.bucketNameExpr, gocf.String("/*")), "Condition": ArbitraryJSONObject{ "StringEquals": ArbitraryJSONObject{ "aws:Referer": gocf.Ref("AWS::AccountId"), }, }, }, }, }, } s3BucketPolicyResourceName := CloudFormationResourceName("SESMessageBodyBucketPolicy", fmt.Sprintf("%#v", storage.bucketNameExpr)) template.AddResource(s3BucketPolicyResourceName, s3BodyStoragePolicy) // Return the name of the bucket policy s.t. the configurator resource // is properly sequenced. The configurator will fail iff the Bucket Policies aren't // applied b/c the SES Rule Actions check PutObject access to S3 buckets return s3BucketPolicyResourceName, nil }
// 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 }
// 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, roleNameMap map[string]*gocf.StringExpr, template *gocf.Template, 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() dependsOn = append(dependsOn, info.RoleDefinition.logicalName()) } lambdaDescription := info.Options.Description if "" == lambdaDescription { lambdaDescription = fmt.Sprintf("%s: %s", serviceName, info.lambdaFnName) } // 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("nodejs"), Timeout: gocf.Integer(info.Options.Timeout), } cfResource := template.AddResource(info.logicalName(), lambdaResource) cfResource.DependsOn = append(cfResource.DependsOn, dependsOn...) safeMetadataInsert(cfResource, "golangFunc", info.lambdaFnName) // 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.logicalName(), template, S3Bucket, S3Key, logger) if nil != err { return err } } // Event Source Mappings hash := sha1.New() for _, eachEventSourceMapping := range info.EventSourceMappings { eventSourceMappingResource := gocf.LambdaEventSourceMapping{ EventSourceArn: gocf.String(eachEventSourceMapping.EventSourceArn), FunctionName: functionAttr, StartingPosition: gocf.String(eachEventSourceMapping.StartingPosition), BatchSize: gocf.Integer(eachEventSourceMapping.BatchSize), Enabled: gocf.Bool(!eachEventSourceMapping.Disabled), } hash.Write([]byte(eachEventSourceMapping.EventSourceArn)) binary.Write(hash, binary.LittleEndian, eachEventSourceMapping.BatchSize) hash.Write([]byte(eachEventSourceMapping.StartingPosition)) resourceName := fmt.Sprintf("LambdaES%s", hex.EncodeToString(hash.Sum(nil))) template.AddResource(resourceName, eventSourceMappingResource) } // Decorator if nil != info.Decorator { logger.Debug("Decorator found for Lambda: ", info.lambdaFnName) // Create an empty template so that we can track whether things // are overwritten decoratorProxyTemplate := gocf.NewTemplate() err := info.Decorator(info.logicalName(), lambdaResource, decoratorProxyTemplate, logger) if nil != err { return err } // Append the custom resources err = safeMergeTemplates(decoratorProxyTemplate, template, logger) if nil != err { return fmt.Errorf("Lambda (%s) decorator created conflicting resources", info.lambdaFnName) } } return nil }
// export marshals the API data to a CloudFormation compatible representation func (s3Site *S3Site) export(S3Bucket string, S3Key string, S3ResourcesKey string, apiGatewayOutputs map[string]*gocf.Output, roleNameMap map[string]*gocf.StringExpr, template *gocf.Template, logger *logrus.Logger) error { websiteConfig := s3Site.WebsiteConfiguration if nil == websiteConfig { websiteConfig = &s3.WebsiteConfiguration{} } ////////////////////////////////////////////////////////////////////////////// // 1 - Create the S3 bucket. The "BucketName" property is empty s.t. // AWS will assign a unique one. if nil == websiteConfig.ErrorDocument { websiteConfig.ErrorDocument = &s3.ErrorDocument{ Key: aws.String("error.html"), } } if nil == websiteConfig.IndexDocument { websiteConfig.IndexDocument = &s3.IndexDocument{ Suffix: aws.String("index.html"), } } s3WebsiteConfig := &gocf.S3WebsiteConfigurationProperty{ ErrorDocument: gocf.String(aws.StringValue(websiteConfig.ErrorDocument.Key)), IndexDocument: gocf.String(aws.StringValue(websiteConfig.IndexDocument.Suffix)), } s3Bucket := &gocf.S3Bucket{ AccessControl: gocf.String("PublicRead"), WebsiteConfiguration: s3WebsiteConfig, } s3BucketResourceName := stableCloudformationResourceName("Site") cfResource := template.AddResource(s3BucketResourceName, s3Bucket) cfResource.DeletionPolicy = "Delete" template.Outputs[OutputS3SiteURL] = &gocf.Output{ Description: "S3 Website URL", Value: gocf.GetAtt(s3BucketResourceName, "WebsiteURL"), } // Represents the S3 ARN that is provisioned s3SiteBucketResourceValue := gocf.Join("", gocf.String("arn:aws:s3:::"), gocf.Ref(s3BucketResourceName)) s3SiteBucketAllKeysResourceValue := gocf.Join("", gocf.String("arn:aws:s3:::"), gocf.Ref(s3BucketResourceName), gocf.String("/*")) ////////////////////////////////////////////////////////////////////////////// // 2 - Add a bucket policy to enable anonymous access, as the PublicRead // canned ACL doesn't seem to do what is implied. // TODO - determine if this is needed or if PublicRead is being misued s3SiteBucketPolicy := &gocf.S3BucketPolicy{ Bucket: gocf.Ref(s3BucketResourceName).String(), PolicyDocument: ArbitraryJSONObject{ "Version": "2012-10-17", "Statement": []ArbitraryJSONObject{ { "Sid": "PublicReadGetObject", "Effect": "Allow", "Principal": ArbitraryJSONObject{ "AWS": "*", }, "Action": "s3:GetObject", "Resource": s3SiteBucketAllKeysResourceValue, }, }, }, } s3BucketPolicyResourceName := stableCloudformationResourceName("S3SiteBucketPolicy") template.AddResource(s3BucketPolicyResourceName, s3SiteBucketPolicy) ////////////////////////////////////////////////////////////////////////////// // 3 - Create the IAM role for the lambda function // The lambda function needs to download the posted resource content, as well // as manage the S3 bucket that hosts the site. statements := CommonIAMStatements["core"] statements = append(statements, iamPolicyStatement{ Action: []string{"s3:ListBucket"}, Effect: "Allow", Resource: s3SiteBucketResourceValue, }) statements = append(statements, iamPolicyStatement{ Action: []string{"s3:DeleteObject", "s3:PutObject"}, Effect: "Allow", Resource: s3SiteBucketAllKeysResourceValue, }) statements = append(statements, iamPolicyStatement{ Action: []string{"s3:GetObject"}, Effect: "Allow", Resource: gocf.Join("", gocf.String("arn:aws:s3:::"), gocf.String(S3Bucket), gocf.String("/"), gocf.String(S3ResourcesKey)), }) iamS3Role := &gocf.IAMRole{ AssumeRolePolicyDocument: AssumePolicyDocument, Policies: &gocf.IAMPoliciesList{ gocf.IAMPolicies{ ArbitraryJSONObject{ "Version": "2012-10-17", "Statement": statements, }, gocf.String("S3SiteMgmnt"), }, }, } iamRoleName := stableCloudformationResourceName("S3SiteIAMRole") cfResource = template.AddResource(iamRoleName, iamS3Role) cfResource.DependsOn = append(cfResource.DependsOn, s3BucketResourceName) iamRoleRef := gocf.GetAtt(iamRoleName, "Arn") ////////////////////////////////////////////////////////////////////////////// // 4 - Create the lambda function definition that executes with the // dynamically provisioned IAM policy customResourceHandlerDef := gocf.LambdaFunction{ Code: &gocf.LambdaFunctionCode{ S3Bucket: gocf.String(S3Bucket), S3Key: gocf.String(S3Key), }, Description: gocf.String("Manage static S3 site resources"), Handler: gocf.String(nodeJSHandlerName("s3Site")), Role: iamRoleRef, Runtime: gocf.String("nodejs"), Timeout: gocf.Integer(30), // Default is 128, but we're buffering everything in memory, in NodeJS MemorySize: gocf.Integer(256), } lambdaResourceName := stableCloudformationResourceName("S3SiteCreator") cfResource = template.AddResource(lambdaResourceName, customResourceHandlerDef) cfResource.DependsOn = append(cfResource.DependsOn, s3BucketResourceName, iamRoleName) ////////////////////////////////////////////////////////////////////////////// // 5 - Create the custom resource that invokes the site bootstrapper lambda to // actually populate the S3 with content customResourceName := stableCloudformationResourceName("S3SiteInvoker") newResource, err := newCloudFormationResource("Custom::SpartaS3SiteManager", logger) if nil != err { return err } customResource := newResource.(*cloudformationS3SiteManager) customResource.ServiceToken = gocf.GetAtt(lambdaResourceName, "Arn") customResource.TargetBucket = s3SiteBucketResourceValue customResource.SourceKey = gocf.String(S3ResourcesKey) customResource.SourceBucket = gocf.String(S3Bucket) customResource.APIGateway = apiGatewayOutputs cfResource = template.AddResource(customResourceName, customResource) cfResource.DependsOn = append(cfResource.DependsOn, lambdaResourceName) return nil }
func (perm CloudWatchLogsPermission) export(serviceName string, lambdaFunctionDisplayName string, lambdaLogicalCFResourceName string, template *gocf.Template, S3Bucket string, S3Key string, logger *logrus.Logger) (string, error) { // If there aren't any expressions to register with? if len(perm.Filters) <= 0 { return "", fmt.Errorf("CloudWatchLogsPermission for function %s does not specify any filters", lambdaFunctionDisplayName) } // The principal is region specific, so build that up... regionalPrincipal := gocf.Join(".", gocf.String("logs"), gocf.Ref("AWS::Region"), gocf.String("amazonaws.com")) // 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(cloudformationLogsSourceArnParts...).String() != wildcardArn.String() { logger.WithFields(logrus.Fields{ "Arn": perm.BasePermission.sourceArnExpr(cloudformationEventsSourceArnParts...), }).Warn("CloudWatchLogs do not support literal ARN values") } // Make sure we grant InvokeFunction privileges to CloudWatchLogs lambdaInvokePermission, err := perm.BasePermission.export(regionalPrincipal, cloudformationLogsSourceArnParts, lambdaFunctionDisplayName, lambdaLogicalCFResourceName, template, S3Bucket, S3Key, logger) if nil != err { return "", err } // Then we need to uniqueify the rule names s.t. we prevent // collisions with other stacks. configurationResourceNames := make(map[string]int, 0) // Store the last name. We'll do a uniqueness check when exiting the loop, // and if that passes, the last name will also be the unique one. var configurationResourceName string // Create the CustomResource entries globallyUniqueFilters := make(map[string]CloudWatchLogsSubscriptionFilter, len(perm.Filters)) for eachFilterName, eachFilter := range perm.Filters { filterPrefix := fmt.Sprintf("%s_%s", serviceName, eachFilterName) uniqueFilterName := CloudFormationResourceName(filterPrefix, lambdaLogicalCFResourceName) globallyUniqueFilters[uniqueFilterName] = eachFilter // The ARN we supply to IAM is built up using the user supplied groupname cloudWatchLogsArn := gocf.Join("", gocf.String("arn:aws:logs:"), gocf.Ref("AWS::Region"), gocf.String(":"), gocf.Ref("AWS::AccountId"), gocf.String(":log-group:"), gocf.String(eachFilter.LogGroupName), gocf.String(":log-stream:*")) lastConfigurationResourceName, ensureCustomHandlerError := ensureCustomResourceHandler(serviceName, cloudformationresources.CloudWatchLogsLambdaEventSource, cloudWatchLogsArn, []string{}, template, S3Bucket, S3Key, logger) if nil != ensureCustomHandlerError { return "", err } configurationResourceNames[configurationResourceName] = 1 configurationResourceName = lastConfigurationResourceName } if len(configurationResourceNames) > 1 { return "", fmt.Errorf("Internal integrity check failed. Multiple configurators (%d) provisioned for CloudWatchLogs", len(configurationResourceNames)) } // Get the single configurator name from the // Add the custom resource that uses this... ////////////////////////////////////////////////////////////////////////////// newResource, newResourceError := newCloudFormationResource(cloudformationresources.CloudWatchLogsLambdaEventSource, logger) if nil != newResourceError { return "", newResourceError } customResource := newResource.(*cloudformationresources.CloudWatchLogsLambdaEventSourceResource) customResource.ServiceToken = gocf.GetAtt(configurationResourceName, "Arn") customResource.LambdaTargetArn = gocf.GetAtt(lambdaLogicalCFResourceName, "Arn") // Build up the filters... customResource.Filters = make([]*cloudformationresources.CloudWatchLogsLambdaEventSourceFilter, 0) for eachName, eachFilter := range globallyUniqueFilters { customResource.Filters = append(customResource.Filters, &cloudformationresources.CloudWatchLogsLambdaEventSourceFilter{ Name: gocf.String(eachName), Pattern: gocf.String(eachFilter.FilterPattern), LogGroupName: gocf.String(eachFilter.LogGroupName), }) } resourceInvokerName := CloudFormationResourceName("ConfigCloudWatchLogs", lambdaLogicalCFResourceName, perm.BasePermission.SourceAccount) // Add it cfResource := template.AddResource(resourceInvokerName, customResource) cfResource.DependsOn = append(cfResource.DependsOn, lambdaInvokePermission, lambdaLogicalCFResourceName, configurationResourceName) return "", nil }
func (perm CloudWatchEventsPermission) export(serviceName string, lambdaFunctionDisplayName string, lambdaLogicalCFResourceName string, template *gocf.Template, S3Bucket string, S3Key string, logger *logrus.Logger) (string, error) { // There needs to be at least one rule to apply if len(perm.Rules) <= 0 { return "", fmt.Errorf("CloudWatchEventsPermission for function %s does not specify any expressions", lambdaFunctionDisplayName) } // 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)) } // Add the permission to invoke the lambda function uniqueRuleNameMap := make(map[string]int, 0) for eachRuleName, eachRuleDefinition := range perm.Rules { // We need a stable unique name s.t. the permission is properly configured... uniqueRuleName := CloudFormationResourceName(eachRuleName, lambdaFunctionDisplayName, serviceName) uniqueRuleNameMap[uniqueRuleName]++ // Add the permission basePerm := BasePermission{ SourceArn: arnPermissionForRuleName(uniqueRuleName), } _, exportErr := basePerm.export(gocf.String(CloudWatchEventsPrincipal), cloudformationEventsSourceArnParts, lambdaFunctionDisplayName, lambdaLogicalCFResourceName, template, S3Bucket, S3Key, logger) if nil != exportErr { return "", exportErr } cwEventsRuleTargetList := gocf.CloudWatchEventsRuleTargetList{} cwEventsRuleTargetList = append(cwEventsRuleTargetList, gocf.CloudWatchEventsRuleTarget{ Arn: gocf.GetAtt(lambdaLogicalCFResourceName, "Arn"), Id: gocf.String(uniqueRuleName), }, ) // Add the rule eventsRule := &gocf.EventsRule{ Name: gocf.String(uniqueRuleName), Description: gocf.String(eachRuleDefinition.Description), Targets: &cwEventsRuleTargetList, } if nil != eachRuleDefinition.EventPattern && "" != eachRuleDefinition.ScheduleExpression { return "", fmt.Errorf("CloudWatchEvents rule %s specifies both EventPattern and ScheduleExpression", eachRuleName) } if nil != eachRuleDefinition.EventPattern { eventsRule.EventPattern = eachRuleDefinition.EventPattern } else if "" != eachRuleDefinition.ScheduleExpression { eventsRule.ScheduleExpression = gocf.String(eachRuleDefinition.ScheduleExpression) } cloudWatchLogsEventResName := CloudFormationResourceName(fmt.Sprintf("%s-CloudWatchEventsRule", eachRuleName), lambdaLogicalCFResourceName, lambdaFunctionDisplayName) template.AddResource(cloudWatchLogsEventResName, eventsRule) } // Validate it for _, eachCount := range uniqueRuleNameMap { if eachCount != 1 { return "", fmt.Errorf("Integrity violation for CloudWatchEvent Rulenames: %#v", uniqueRuleNameMap) } } return "", nil }
func (perm SESPermission) 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(SESPrincipal), sesSourcePartArn, lambdaFunctionDisplayName, lambdaLogicalCFResourceName, template, S3Bucket, S3Key, logger) if nil != err { return "", err } // MessageBody storage? var dependsOn []string if nil != perm.MessageBodyStorage { s3Policy, s3PolicyErr := perm.MessageBodyStorage.export(serviceName, lambdaFunctionDisplayName, lambdaLogicalCFResourceName, template, S3Bucket, S3Key, logger) if nil != s3PolicyErr { return "", s3PolicyErr } if "" != s3Policy { dependsOn = append(dependsOn, s3Policy) } } // Make sure the custom lambda that manages SNS notifications is provisioned. configuratorResName, err := ensureCustomResourceHandler(serviceName, cloudformationresources.SESLambdaEventSource, sourceArnExpression, dependsOn, template, S3Bucket, S3Key, logger) if nil != err { return "", err } // Add a custom resource invocation for this configuration ////////////////////////////////////////////////////////////////////////////// newResource, newResourceError := newCloudFormationResource(cloudformationresources.SESLambdaEventSource, logger) if nil != newResourceError { return "", newResourceError } customResource := newResource.(*cloudformationresources.SESLambdaEventSourceResource) customResource.ServiceToken = gocf.GetAtt(configuratorResName, "Arn") // The shared ruleset name used by all Sparta applications customResource.RuleSetName = gocf.String("SpartaRuleSet") /////////////////// // Build up the Rules // If there aren't any rules, make one that forwards everything... var sesRules []*cloudformationresources.SESLambdaEventSourceResourceRule if nil == perm.ReceiptRules { sesRules = append(sesRules, &cloudformationresources.SESLambdaEventSourceResourceRule{ Name: gocf.String("Default"), Actions: make([]*cloudformationresources.SESLambdaEventSourceResourceAction, 0), ScanEnabled: gocf.Bool(false), Enabled: gocf.Bool(true), Recipients: []*gocf.StringExpr{}, TLSPolicy: gocf.String("Optional"), }) } // Append all the user defined ones for _, eachReceiptRule := range perm.ReceiptRules { sesRules = append(sesRules, eachReceiptRule.toResourceRule( serviceName, gocf.GetAtt(lambdaLogicalCFResourceName, "Arn"), perm.MessageBodyStorage)) } customResource.Rules = sesRules // Name? resourceInvokerName := CloudFormationResourceName("ConfigSNS", lambdaLogicalCFResourceName, perm.BasePermission.SourceAccount) // Add it cfResource := template.AddResource(resourceInvokerName, customResource) cfResource.DependsOn = append(cfResource.DependsOn, targetLambdaResourceName, configuratorResName) return "", nil }
// TODO - Refactor ensure Lambdaconfigurator, then finish // implementing the CloudWatchEvents Principal type. func ensureConfiguratorLambdaResource(awsPrincipalName string, sourceArn *gocf.StringExpr, dependsOn []string, template *gocf.Template, S3Bucket string, S3Key string, logger *logrus.Logger) (string, error) { // AWS service basename awsServiceName := awsPrincipalToService(awsPrincipalName) configuratorExportName := strings.ToLower(awsServiceName) logger.WithFields(logrus.Fields{ "ServiceName": awsServiceName, "NodeJSExportName": configuratorExportName, }).Debug("Ensuring AWS push service configurator CustomResource") // 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": awsPrincipalName, "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(awsPrincipalName, sourceArn, template, logger) if nil != err { return "", err } iamRoleRef := gocf.GetAtt(iamResourceName, "Arn") _, exists := template.Resources[subscriberHandlerName] if !exists { logger.WithFields(logrus.Fields{ "Service": awsServiceName, }).Debug("Including Lambda CustomResource for AWS Service") configuratorDescription := fmt.Sprintf("Sparta created Lambda CustomResource to configure %s service", awsServiceName) ////////////////////////////////////////////////////////////////////////////// // Custom Resource Lambda Handler // NOTE: This brittle function name has an analog in ./resources/index.js b/c the // AWS Lamba execution treats the entire ZIP file as a module. So all module exports // need to be forwarded through the module's index.js file. handlerName := nodeJSHandlerName(configuratorExportName) logger.Debug("Lambda Configuration handler: ", handlerName) 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("nodejs"), Timeout: gocf.Integer(30), } cfResource := template.AddResource(subscriberHandlerName, customResourceHandlerDef) if nil != dependsOn && (len(dependsOn) > 0) { cfResource.DependsOn = append(cfResource.DependsOn, dependsOn...) } } return subscriberHandlerName, nil }
func (perm S3Permission) export(serviceName string, lambdaLogicalCFResourceName string, template *gocf.Template, S3Bucket string, S3Key string, logger *logrus.Logger) (string, error) { targetLambdaResourceName, err := perm.BasePermission.export(S3Principal, s3SourceArnParts, lambdaLogicalCFResourceName, template, S3Bucket, S3Key, logger) if nil != err { return "", err } // Make sure the custom lambda that manages s3 notifications is provisioned. sourceArnExpression := perm.BasePermission.sourceArnExpr(s3SourceArnParts...) configuratorResName, err := ensureConfiguratorLambdaResource(S3Principal, sourceArnExpression, []string{}, template, S3Bucket, S3Key, logger) if nil != err { return "", err } permissionData := ArbitraryJSONObject{ "Events": perm.Events, } if nil != perm.Filter.Key { permissionData["Filter"] = perm.Filter } // Add a custom resource invocation for this configuration ////////////////////////////////////////////////////////////////////////////// // And finally the custom resource forwarder newResource, err := newCloudFormationResource("Custom::SpartaS3Permission", logger) if nil != err { return "", err } customResource := newResource.(*cloudFormationS3PermissionResource) customResource.ServiceToken = gocf.GetAtt(configuratorResName, "Arn") customResource.Permission = permissionData customResource.LambdaTarget = gocf.GetAtt(lambdaLogicalCFResourceName, "Arn") customResource.BucketArn = sourceArnExpression // Name? resourceInvokerName := CloudFormationResourceName("ConfigS3", targetLambdaResourceName, perm.BasePermission.SourceAccount, fmt.Sprintf("%v", sourceArnExpression)) // Add it cfResource := template.AddResource(resourceInvokerName, customResource) cfResource.DependsOn = append(cfResource.DependsOn, targetLambdaResourceName, configuratorResName) return "", nil }
func (perm SNSPermission) export(serviceName 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(SNSPrincipal, snsSourceArnParts, lambdaLogicalCFResourceName, template, S3Bucket, S3Key, logger) if nil != err { return "", err } // Make sure the custom lambda that manages SNS notifications is provisioned. configuratorResName, err := ensureConfiguratorLambdaResource(SNSPrincipal, sourceArnExpression, []string{}, template, S3Bucket, S3Key, logger) if nil != err { return "", err } // Add a custom resource invocation for this configuration ////////////////////////////////////////////////////////////////////////////// // And the custom resource forwarder newResource, err := newCloudFormationResource("Custom::SpartaSNSPermission", logger) if nil != err { return "", err } customResource := newResource.(*cloudFormationSNSPermissionResource) customResource.ServiceToken = gocf.GetAtt(configuratorResName, "Arn") customResource.Mode = "Subscribe" customResource.TopicArn = sourceArnExpression customResource.LambdaTarget = gocf.GetAtt(lambdaLogicalCFResourceName, "Arn") subscriberResourceName := CloudFormationResourceName("SubscriberSNS", targetLambdaResourceName, perm.BasePermission.SourceAccount, fmt.Sprintf("%v", perm.BasePermission.SourceArn)) cfResource := template.AddResource(subscriberResourceName, customResource) cfResource.DependsOn = append(cfResource.DependsOn, targetLambdaResourceName, configuratorResName) ////////////////////////////////////////////////////////////////////////////// // And the custom resource unsubscriber newResource, err = newCloudFormationResource("Custom::SpartaSNSPermission", logger) if nil != err { return "", err } customResource = newResource.(*cloudFormationSNSPermissionResource) customResource.ServiceToken = gocf.GetAtt(configuratorResName, "Arn") customResource.Mode = "Unsubscribe" customResource.TopicArn = sourceArnExpression customResource.LambdaTarget = gocf.GetAtt(lambdaLogicalCFResourceName, "Arn") unsubscriberResourceName := CloudFormationResourceName("UnsubscriberSNS", targetLambdaResourceName) template.AddResource(unsubscriberResourceName, customResource) return "", nil }
// 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 }
// export marshals the API data to a CloudFormation compatible representation func (s3Site *S3Site) export(serviceName string, S3Bucket string, S3Key string, S3ResourcesKey string, apiGatewayOutputs map[string]*gocf.Output, roleNameMap map[string]*gocf.StringExpr, template *gocf.Template, logger *logrus.Logger) error { websiteConfig := s3Site.WebsiteConfiguration if nil == websiteConfig { websiteConfig = &s3.WebsiteConfiguration{} } ////////////////////////////////////////////////////////////////////////////// // 1 - Create the S3 bucket. The "BucketName" property is empty s.t. // AWS will assign a unique one. if nil == websiteConfig.ErrorDocument { websiteConfig.ErrorDocument = &s3.ErrorDocument{ Key: aws.String("error.html"), } } if nil == websiteConfig.IndexDocument { websiteConfig.IndexDocument = &s3.IndexDocument{ Suffix: aws.String("index.html"), } } s3WebsiteConfig := &gocf.S3WebsiteConfigurationProperty{ ErrorDocument: gocf.String(aws.StringValue(websiteConfig.ErrorDocument.Key)), IndexDocument: gocf.String(aws.StringValue(websiteConfig.IndexDocument.Suffix)), } s3Bucket := &gocf.S3Bucket{ AccessControl: gocf.String("PublicRead"), WebsiteConfiguration: s3WebsiteConfig, } s3BucketResourceName := stableCloudformationResourceName("Site") cfResource := template.AddResource(s3BucketResourceName, s3Bucket) cfResource.DeletionPolicy = "Delete" template.Outputs[OutputS3SiteURL] = &gocf.Output{ Description: "S3 Website URL", Value: gocf.GetAtt(s3BucketResourceName, "WebsiteURL"), } // Represents the S3 ARN that is provisioned s3SiteBucketResourceValue := gocf.Join("", gocf.String("arn:aws:s3:::"), gocf.Ref(s3BucketResourceName)) s3SiteBucketAllKeysResourceValue := gocf.Join("", gocf.String("arn:aws:s3:::"), gocf.Ref(s3BucketResourceName), gocf.String("/*")) ////////////////////////////////////////////////////////////////////////////// // 2 - Add a bucket policy to enable anonymous access, as the PublicRead // canned ACL doesn't seem to do what is implied. // TODO - determine if this is needed or if PublicRead is being misued s3SiteBucketPolicy := &gocf.S3BucketPolicy{ Bucket: gocf.Ref(s3BucketResourceName).String(), PolicyDocument: ArbitraryJSONObject{ "Version": "2012-10-17", "Statement": []ArbitraryJSONObject{ { "Sid": "PublicReadGetObject", "Effect": "Allow", "Principal": ArbitraryJSONObject{ "AWS": "*", }, "Action": "s3:GetObject", "Resource": s3SiteBucketAllKeysResourceValue, }, }, }, } s3BucketPolicyResourceName := stableCloudformationResourceName("S3SiteBucketPolicy") template.AddResource(s3BucketPolicyResourceName, s3SiteBucketPolicy) ////////////////////////////////////////////////////////////////////////////// // 3 - Create the IAM role for the lambda function // The lambda function needs to download the posted resource content, as well // as manage the S3 bucket that hosts the site. statements := CommonIAMStatements.Core statements = append(statements, spartaIAM.PolicyStatement{ Action: []string{"s3:ListBucket", "s3:ListObjectsPages"}, Effect: "Allow", Resource: s3SiteBucketResourceValue, }) statements = append(statements, spartaIAM.PolicyStatement{ Action: []string{"s3:DeleteObject", "s3:PutObject", "s3:DeleteObjects", "s3:DeleteObjects"}, Effect: "Allow", Resource: s3SiteBucketAllKeysResourceValue, }) statements = append(statements, spartaIAM.PolicyStatement{ Action: []string{"s3:GetObject"}, Effect: "Allow", Resource: gocf.Join("", gocf.String("arn:aws:s3:::"), gocf.String(S3Bucket), gocf.String("/"), gocf.String(S3ResourcesKey)), }) iamPolicyList := gocf.IAMPoliciesList{} iamPolicyList = append(iamPolicyList, gocf.IAMPolicies{ PolicyDocument: ArbitraryJSONObject{ "Version": "2012-10-17", "Statement": statements, }, PolicyName: gocf.String("S3SiteMgmnt"), }, ) iamS3Role := &gocf.IAMRole{ AssumeRolePolicyDocument: AssumePolicyDocument, Policies: &iamPolicyList, } iamRoleName := stableCloudformationResourceName("S3SiteIAMRole") cfResource = template.AddResource(iamRoleName, iamS3Role) cfResource.DependsOn = append(cfResource.DependsOn, s3BucketResourceName) iamRoleRef := gocf.GetAtt(iamRoleName, "Arn") // Create the IAM role and CustomAction handler to do the work ////////////////////////////////////////////////////////////////////////////// // 4 - Create the lambda function definition that executes with the // dynamically provisioned IAM policy. This is similar to what happens in // ensureCustomResourceHandler, but due to the more complex IAM rules // there's a bit of duplication handlerName := lambdaExportNameForCustomResourceType(cloudformationresources.ZipToS3Bucket) logger.WithFields(logrus.Fields{ "CustomResourceType": cloudformationresources.ZipToS3Bucket, "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(customResourceDescription(serviceName, "S3 static site")), Handler: gocf.String(handlerName), Role: iamRoleRef, Runtime: gocf.String(NodeJSVersion), MemorySize: gocf.Integer(256), Timeout: gocf.Integer(180), } lambdaResourceName := stableCloudformationResourceName("S3SiteCreator") cfResource = template.AddResource(lambdaResourceName, customResourceHandlerDef) cfResource.DependsOn = append(cfResource.DependsOn, s3BucketResourceName, iamRoleName) ////////////////////////////////////////////////////////////////////////////// // 5 - Create the custom resource that invokes the site bootstrapper lambda to // actually populate the S3 with content customResourceName := stableCloudformationResourceName("S3SiteBuilder") newResource, err := newCloudFormationResource(cloudformationresources.ZipToS3Bucket, logger) if nil != err { return err } zipResource := newResource.(*cloudformationresources.ZipToS3BucketResource) zipResource.ServiceToken = gocf.GetAtt(lambdaResourceName, "Arn") zipResource.SrcKeyName = gocf.String(S3ResourcesKey) zipResource.SrcBucket = gocf.String(S3Bucket) zipResource.DestBucket = gocf.Ref(s3BucketResourceName).String() // Build the manifest data with any output info... manifestData := make(map[string]interface{}, 0) for eachKey, eachOutput := range apiGatewayOutputs { manifestData[eachKey] = map[string]interface{}{ "Description": eachOutput.Description, "Value": eachOutput.Value, } } zipResource.Manifest = manifestData cfResource = template.AddResource(customResourceName, zipResource) cfResource.DependsOn = append(cfResource.DependsOn, lambdaResourceName, s3BucketResourceName) return nil }