Example #1
0
func annotateDiscoveryInfo(template *gocf.Template, logger *logrus.Logger) *gocf.Template {
	for eachResourceID, eachResource := range template.Resources {
		// Only apply this to lambda functions
		if eachResource.Properties.CfnResourceType() == "AWS::Lambda::Function" {

			// Update the metdata with a reference to the output of each
			// depended on item...
			for _, eachDependsKey := range eachResource.DependsOn {
				dependencyOutputs, _ := outputsForResource(template, eachDependsKey, logger)
				if nil != dependencyOutputs && len(dependencyOutputs) != 0 {
					logger.WithFields(logrus.Fields{
						"Resource":  eachDependsKey,
						"DependsOn": eachResource.DependsOn,
						"Outputs":   dependencyOutputs,
					}).Debug("Resource metadata")
					safeMetadataInsert(eachResource, eachDependsKey, dependencyOutputs)
				}
			}
			// Also include standard AWS outputs at a resource level if a lambda
			// needs to self-discover other resources
			safeMetadataInsert(eachResource, TagLogicalResourceID, gocf.String(eachResourceID))
			safeMetadataInsert(eachResource, TagStackRegion, gocf.Ref("AWS::Region"))
			safeMetadataInsert(eachResource, TagStackID, gocf.Ref("AWS::StackId"))
			safeMetadataInsert(eachResource, TagStackName, gocf.Ref("AWS::StackName"))
		}
	}
	return template
}
Example #2
0
func parseFnJoinExpr(data map[string]interface{}) (*gocf.StringExpr, error) {
	if len(data) <= 0 {
		return nil, fmt.Errorf("FnJoinExpr data is empty")
	}
	for eachKey, eachValue := range data {
		switch eachKey {
		case "Ref":
			return gocf.Ref(eachValue.(string)).String(), nil
		case "Fn::GetAtt":
			attrValues, attrValuesErr := toExpressionSlice(eachValue)
			if nil != attrValuesErr {
				return nil, attrValuesErr
			}
			if len(attrValues) != 2 {
				return nil, fmt.Errorf("Invalid params for Fn::GetAtt: %s", eachValue)
			}
			return gocf.GetAtt(attrValues[0], attrValues[1]).String(), nil
		case "Fn::FindInMap":
			attrValues, attrValuesErr := toExpressionSlice(eachValue)
			if nil != attrValuesErr {
				return nil, attrValuesErr
			}
			if len(attrValues) != 3 {
				return nil, fmt.Errorf("Invalid params for Fn::FindInMap: %s", eachValue)
			}
			return gocf.FindInMap(attrValues[0], gocf.String(attrValues[1]), gocf.String(attrValues[2])), nil
		}
	}
	return nil, fmt.Errorf("Unsupported AWS Function detected: %#v", data)
}
Example #3
0
func appendDynamicSNSLambda(api *sparta.API, lambdaFunctions []*sparta.LambdaAWSInfo) []*sparta.LambdaAWSInfo {
	snsTopicName := sparta.CloudFormationResourceName("SNSDynamicTopic")
	lambdaFn := sparta.NewLambda(sparta.IAMRoleDefinition{}, echoDynamicSNSEvent, nil)
	lambdaFn.Permissions = append(lambdaFn.Permissions, sparta.SNSPermission{
		BasePermission: sparta.BasePermission{
			SourceArn: gocf.Ref(snsTopicName),
		},
	})

	lambdaFn.Decorator = func(serviceName string,
		lambdaResourceName string,
		lambdaResource gocf.LambdaFunction,
		resourceMetadata map[string]interface{},
		S3Bucket string,
		S3Key string,
		buildID string,
		template *gocf.Template,
		context map[string]interface{},
		logger *logrus.Logger) error {
		template.AddResource(snsTopicName, &gocf.SNSTopic{
			DisplayName: gocf.String("Sparta Application SNS topic"),
		})
		return nil
	}
	return append(lambdaFunctions, lambdaFn)
}
Example #4
0
func appendDynamicS3BucketLambda(api *sparta.API, lambdaFunctions []*sparta.LambdaAWSInfo) []*sparta.LambdaAWSInfo {

	s3BucketResourceName := sparta.CloudFormationResourceName("S3DynamicBucket")

	lambdaFn := sparta.NewLambda(sparta.IAMRoleDefinition{}, echoS3DynamicBucketEvent, nil)
	lambdaFn.Permissions = append(lambdaFn.Permissions, sparta.S3Permission{
		BasePermission: sparta.BasePermission{
			SourceArn: gocf.Ref(s3BucketResourceName),
		},
		Events: []string{"s3:ObjectCreated:*", "s3:ObjectRemoved:*"},
	})
	lambdaFn.DependsOn = append(lambdaFn.DependsOn, s3BucketResourceName)

	// Add permission s.t. the lambda function could read from the S3 bucket
	lambdaFn.RoleDefinition.Privileges = append(lambdaFn.RoleDefinition.Privileges,
		sparta.IAMRolePrivilege{
			Actions:  []string{"s3:GetObject", "s3:HeadObject"},
			Resource: spartaCF.S3AllKeysArnForBucket(gocf.Ref(s3BucketResourceName)),
		})

	lambdaFn.Decorator = func(serviceName string,
		lambdaResourceName string,
		lambdaResource gocf.LambdaFunction,
		resourceMetadata map[string]interface{},
		S3Bucket string,
		S3Key string,
		buildID string,
		template *gocf.Template,
		context map[string]interface{},
		logger *logrus.Logger) error {
		cfResource := template.AddResource(s3BucketResourceName, &gocf.S3Bucket{
			AccessControl: gocf.String("PublicRead"),
			Tags: []gocf.ResourceTag{
				gocf.ResourceTag{
					Key:   gocf.String("SpecialKey"),
					Value: gocf.String("SpecialValue"),
				},
			},
		})
		cfResource.DeletionPolicy = "Delete"
		return nil
	}
	return append(lambdaFunctions, lambdaFn)
}
Example #5
0
// NewMessageBodyStorageResource provisions a new S3 bucket to store message body
// content.
func (perm *SESPermission) NewMessageBodyStorageResource(bucketLogicalName string) (*MessageBodyStorage, error) {
	if len(bucketLogicalName) <= 0 {
		return nil, errors.New("NewMessageBodyStorageResource requires a unique, non-empty `bucketLogicalName` parameter ")
	}
	store := &MessageBodyStorage{
		logicalBucketName: bucketLogicalName,
	}
	store.cloudFormationS3BucketResourceName = CloudFormationResourceName("SESMessageStoreBucket", bucketLogicalName)
	store.bucketNameExpr = gocf.Ref(store.cloudFormationS3BucketResourceName).String()
	return store, nil
}
Example #6
0
func makeTemplate() *cf.Template {
	t := cf.NewTemplate()
	t.Description = "example production infrastructure"
	t.Parameters["DnsName"] = &cf.Parameter{
		Description: "The top level DNS name for the infrastructure",
		Type:        "String",
		Default:     "preview.example.io",
	}

	t.AddResource("ServerLoadBalancer", cf.ElasticLoadBalancingLoadBalancer{
		ConnectionDrainingPolicy: &cf.ElasticLoadBalancingConnectionDrainingPolicy{
			Enabled: cf.Bool(true),
			Timeout: cf.Integer(30),
		},
		CrossZone: cf.Bool(true),
		HealthCheck: &cf.ElasticLoadBalancingHealthCheck{
			HealthyThreshold:   cf.String("2"),
			Interval:           cf.String("60"),
			Target:             cf.String("HTTP:80/"),
			Timeout:            cf.String("5"),
			UnhealthyThreshold: cf.String("2"),
		},
		Listeners: &cf.ElasticLoadBalancingListenerList{
			cf.ElasticLoadBalancingListener{
				InstancePort:     cf.String("8000"),
				InstanceProtocol: cf.String("TCP"),
				LoadBalancerPort: cf.String("443"),
				Protocol:         cf.String("SSL"),
				SSLCertificateId: cf.Join("",
					cf.String("arn:aws:iam::"),
					cf.Ref("AWS::AccountID"),
					cf.String(":server-certificate/"),
					cf.Ref("DnsName")),
			},
		},
		Policies: &cf.ElasticLoadBalancingPolicyList{
			cf.ElasticLoadBalancingPolicy{
				PolicyName: cf.String("EnableProxyProtocol"),
				PolicyType: cf.String("ProxyProtocolPolicyType"),
				Attributes: []map[string]interface{}{
					map[string]interface{}{
						"Name":  "ProxyProtocol",
						"Value": "true",
					},
				},
				InstancePorts: []int{8000},
			},
		},
		Subnets: cf.StringList(
			cf.Ref("VpcSubnetA"),
			cf.Ref("VpcSubnetB"),
			cf.Ref("VpcSubnetC"),
		),
		SecurityGroups: cf.StringList(cf.Ref("LoadBalancerSecurityGroup")),
	})

	return t
}
Example #7
0
func annotateCodePipelineEnvironments(lambdaAWSInfo *LambdaAWSInfo, logger *logrus.Logger) {
	if nil != codePipelineEnvironments {
		if nil == lambdaAWSInfo.Options {
			lambdaAWSInfo.Options = defaultLambdaFunctionOptions()
		}
		if nil == lambdaAWSInfo.Options.Environment {
			lambdaAWSInfo.Options.Environment = make(map[string]*gocf.StringExpr, 0)
		}
		for _, eachEnvironment := range codePipelineEnvironments {

			logger.WithFields(logrus.Fields{
				"Environment":    eachEnvironment,
				"LambdaFunction": lambdaAWSInfo.lambdaFunctionName(),
			}).Debug("Annotating Lambda environment for CodePipeline")

			for eachKey := range eachEnvironment {
				lambdaAWSInfo.Options.Environment[eachKey] = gocf.Ref(eachKey).String()
			}
		}
	}
}
func outputsForResource(template *gocf.Template,
	logicalResourceName string,
	logger *logrus.Logger) (map[string]interface{}, error) {

	item, ok := template.Resources[logicalResourceName]
	if !ok {
		return nil, nil
	}

	outputs := make(map[string]interface{}, 0)
	attrs, exists := cloudformationTypeMapDiscoveryOutputs[item.Properties.ResourceType()]
	if exists {
		outputs["Ref"] = gocf.Ref(logicalResourceName).String()
		outputs[TagResourceType] = item.Properties.ResourceType()

		for _, eachAttr := range attrs {
			outputs[eachAttr] = gocf.GetAtt(logicalResourceName, eachAttr)
		}

		// Any tags?
		r := reflect.ValueOf(item.Properties)
		tagsField := reflect.Indirect(r).FieldByName("Tags")
		if tagsField.IsValid() && !tagsField.IsNil() {
			outputs["Tags"] = tagsField.Interface()
		}
	}

	if len(outputs) != 0 {
		logger.WithFields(logrus.Fields{
			"ResourceName": logicalResourceName,
			"Outputs":      outputs,
		}).Debug("Resource Outputs")
	}

	return outputs, nil
}
Example #9
0
	}
}

func init() {
	gocf.RegisterCustomResourceProvider(customResourceProvider)
	rand.Seed(time.Now().Unix())
}

////////////////////////////////////////////////////////////////////////////////
// Variables
////////////////////////////////////////////////////////////////////////////////

// Represents the CloudFormation Arn of this stack, referenced
// in CommonIAMStatements
var cloudFormationThisStackArn = []gocf.Stringable{gocf.String("arn:aws:cloudformation:"),
	gocf.Ref("AWS::Region").String(),
	gocf.String(":"),
	gocf.Ref("AWS::AccountId").String(),
	gocf.String(":stack/"),
	gocf.Ref("AWS::StackName").String(),
	gocf.String("/*")}

// CommonIAMStatements defines common IAM::Role Policy Statement values for different AWS
// service types.  See http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#genref-aws-service-namespaces
// for names.
// http://docs.aws.amazon.com/lambda/latest/dg/monitoring-functions.html
// for more information.
var CommonIAMStatements = struct {
	Core     []spartaIAM.PolicyStatement
	VPC      []spartaIAM.PolicyStatement
	DynamoDB []spartaIAM.PolicyStatement
Example #10
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 #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
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 #13
0
func ensureCloudFormationStack() workflowStep {
	return func(ctx *workflowContext) (workflowStep, error) {
		for _, eachEntry := range ctx.lambdaAWSInfos {
			err := eachEntry.export(ctx.serviceName,
				ctx.s3Bucket,
				ctx.s3LambdaZipKey,
				ctx.lambdaIAMRoleNameMap,
				ctx.cfTemplate,
				ctx.logger)
			if nil != err {
				return nil, err
			}
		}
		// If there's an API gateway definition, include the resources that provision it. Since this export will likely
		// generate outputs that the s3 site needs, we'll use a temporary outputs accumulator, pass that to the S3Site
		// if it's defined, and then merge it with the normal output map.
		apiGatewayTemplate := gocf.NewTemplate()

		if nil != ctx.api {
			err := ctx.api.export(ctx.s3Bucket,
				ctx.s3LambdaZipKey,
				ctx.lambdaIAMRoleNameMap,
				apiGatewayTemplate,
				ctx.logger)
			if nil == err {
				err = safeMergeTemplates(apiGatewayTemplate, ctx.cfTemplate, ctx.logger)
			}
			if nil != err {
				return nil, fmt.Errorf("Failed to export APIGateway template resources")
			}
		}
		// If there's a Site defined, include the resources the provision it
		if nil != ctx.s3SiteContext.s3Site {
			ctx.s3SiteContext.s3Site.export(ctx.s3Bucket,
				ctx.s3LambdaZipKey,
				ctx.s3SiteContext.s3SiteLambdaZipKey,
				apiGatewayTemplate.Outputs,
				ctx.lambdaIAMRoleNameMap,
				ctx.cfTemplate,
				ctx.logger)
		}

		// Save the output
		ctx.cfTemplate.Outputs[OutputSpartaHomeKey] = &gocf.Output{
			Description: "Sparta Home",
			Value:       gocf.String("http://gosparta.io"),
		}
		ctx.cfTemplate.Outputs[OutputSpartaVersionKey] = &gocf.Output{
			Description: "Sparta Version",
			Value:       gocf.String(SpartaVersion),
		}

		// Next pass - exchange outputs between dependencies.  Lambda functions
		for _, eachResource := range ctx.cfTemplate.Resources {
			// Only apply this to lambda functions
			if eachResource.Properties.ResourceType() == "AWS::Lambda::Function" {
				// Update the metdata with a reference to the output of each
				// depended on item...
				for _, eachDependsKey := range eachResource.DependsOn {
					dependencyOutputs, _ := outputsForResource(ctx.cfTemplate, eachDependsKey, ctx.logger)
					if nil != dependencyOutputs && len(dependencyOutputs) != 0 {
						ctx.logger.WithFields(logrus.Fields{
							"Resource":  eachDependsKey,
							"DependsOn": eachResource.DependsOn,
							"Outputs":   dependencyOutputs,
						}).Debug("Resource metadata")
						safeMetadataInsert(eachResource, eachDependsKey, dependencyOutputs)
					}
				}
				// Also include standard AWS outputs at a resource level if a lambda
				// needs to self-discover other resources
				safeMetadataInsert(eachResource, TagStackRegion, gocf.Ref("AWS::Region"))
				safeMetadataInsert(eachResource, TagStackID, gocf.Ref("AWS::StackId"))
				safeMetadataInsert(eachResource, TagStackName, gocf.Ref("AWS::StackName"))
			}
		}

		// Generate a complete CloudFormation template
		cfTemplate, err := json.Marshal(ctx.cfTemplate)
		if err != nil {
			ctx.logger.Error("Failed to Marshal CloudFormation template: ", err.Error())
			return nil, err
		}

		// Upload the actual CloudFormation template to S3 to increase the template
		// size limit
		contentBody := string(cfTemplate)
		sanitizedServiceName := sanitizedName(ctx.serviceName)
		hash := sha1.New()
		hash.Write([]byte(contentBody))
		s3keyName := fmt.Sprintf("%s-%s-cf.json", sanitizedServiceName, hex.EncodeToString(hash.Sum(nil)))

		uploadInput := &s3manager.UploadInput{
			Bucket:      &ctx.s3Bucket,
			Key:         &s3keyName,
			ContentType: aws.String("application/json"),
			Body:        strings.NewReader(contentBody),
		}
		formatted, err := json.MarshalIndent(contentBody, "", " ")
		if nil != err {
			return nil, err
		}

		ctx.logger.WithFields(logrus.Fields{
			"Body": string(formatted),
		}).Debug("CloudFormation template body")

		if nil != ctx.templateWriter {
			io.WriteString(ctx.templateWriter, string(formatted))
		}

		if ctx.noop {
			ctx.logger.WithFields(logrus.Fields{
				"Bucket": ctx.s3Bucket,
				"Key":    s3keyName,
			}).Info("Bypassing template upload & creation due to -n/-noop command line argument")
		} else {
			ctx.logger.Info("Uploading CloudFormation template")
			uploader := s3manager.NewUploader(ctx.awsSession)
			templateUploadResult, err := uploader.Upload(uploadInput)
			if nil != err {
				return nil, err
			}
			// Cleanup if there's a problem
			ctx.registerRollback(createS3RollbackFunc(ctx.awsSession, ctx.s3Bucket, s3keyName, ctx.noop))

			// Be transparent
			ctx.logger.WithFields(logrus.Fields{
				"URL": templateUploadResult.Location,
			}).Info("Template uploaded")

			stack, err := convergeStackState(templateUploadResult.Location, ctx)
			if nil != err {
				return nil, err
			}
			ctx.logger.WithFields(logrus.Fields{
				"StackName":    *stack.StackName,
				"StackId":      *stack.StackId,
				"CreationTime": *stack.CreationTime,
			}).Info("Stack provisioned")
		}
		return nil, nil
	}
}
Example #14
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 #15
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 #16
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 #17
0
}

func (perm BasePermission) descriptionInfo(b *bytes.Buffer, logger *logrus.Logger) error {
	return errors.New("Describe not implemented")
}

//
// END - BasePermission
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// START - LambdaPermission
//
var lambdaSourceArnParts = []gocf.Stringable{
	gocf.String("arn:aws:lambda:"),
	gocf.Ref("AWS::Region"),
	gocf.String(":function:"),
}

// LambdaPermission type that creates a Lambda::Permission entry
// in the generated template, but does NOT automatically register the lambda
// with the BasePermission.SourceArn.  Typically used to register lambdas with
// externally managed event producers
type LambdaPermission struct {
	BasePermission
	// The entity for which you are granting permission to invoke the Lambda function
	Principal string
}

func (perm LambdaPermission) export(serviceName string,
	lambdaLogicalCFResourceName string,
Example #18
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
}