Example #1
0
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
}
Example #2
0
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
}
Example #3
0
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
}
Example #4
0
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
}
Example #5
0
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
}
Example #6
0
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
}
Example #7
0
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
}
Example #8
0
// 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)
}
Example #9
0
// 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
}
Example #10
0
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
}
Example #11
0
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
}
Example #12
0
// 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
}
Example #13
0
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
}
Example #14
0
// 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
}
Example #15
0
// 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
}
Example #16
0
// 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
}
Example #17
0
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
}
Example #18
0
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
}
Example #19
0
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
}
Example #20
0
// 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
}
Example #21
0
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
}
Example #22
0
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
}
Example #23
0
// 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
}
Example #24
0
// 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
}