Example #1
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 #2
0
// S3ArnForBucket returns a CloudFormation-compatible Arn expression
// (string or Ref) suitable for template reference.  The bucket
// parameter may be either a string or an interface{} ("Ref: "myResource")
// value
func S3ArnForBucket(bucket interface{}) *gocf.StringExpr {
	arnParts := []gocf.Stringable{gocf.String("arn:aws:s3:::")}

	switch bucket.(type) {
	case string:
		// Don't be smart if the Arn value is a user supplied literal
		arnParts = append(arnParts, gocf.String(bucket.(string)))
	case *gocf.StringExpr:
		arnParts = append(arnParts, bucket.(*gocf.StringExpr))
	case gocf.RefFunc:
		arnParts = append(arnParts, bucket.(gocf.RefFunc).String())
	default:
		panic(fmt.Sprintf("Unsupported SourceArn value type: %+v", bucket))
	}
	return gocf.Join("", arnParts...).String()
}
Example #3
0
func (perm *BasePermission) sourceArnExpr(joinParts ...gocf.Stringable) *gocf.StringExpr {
	var parts []gocf.Stringable
	if nil != joinParts {
		parts = append(parts, joinParts...)
	}
	switch perm.SourceArn.(type) {
	case string:
		// Don't be smart if the Arn value is a user supplied literal
		parts = []gocf.Stringable{gocf.String(perm.SourceArn.(string))}
	case *gocf.StringExpr:
		parts = append(parts, perm.SourceArn.(*gocf.StringExpr))
	case gocf.RefFunc:
		parts = append(parts, perm.SourceArn.(gocf.RefFunc).String())
	default:
		panic(fmt.Sprintf("Unsupported SourceArn value type: %+v", perm.SourceArn))
	}
	return gocf.Join("", parts...)
}
Example #4
0
// for more information.
var CommonIAMStatements = struct {
	Core     []spartaIAM.PolicyStatement
	VPC      []spartaIAM.PolicyStatement
	DynamoDB []spartaIAM.PolicyStatement
	Kinesis  []spartaIAM.PolicyStatement
}{
	Core: []spartaIAM.PolicyStatement{
		{
			Action: []string{"logs:CreateLogGroup",
				"logs:CreateLogStream",
				"logs:PutLogEvents"},
			Effect: "Allow",
			Resource: gocf.Join("",
				gocf.String("arn:aws:logs:"),
				gocf.Ref("AWS::Region"),
				gocf.String(":"),
				gocf.Ref("AWS::AccountId"),
				gocf.String("*")),
		},
		{
			Action:   []string{"cloudwatch:PutMetricData"},
			Effect:   "Allow",
			Resource: wildcardArn,
		},
		{
			Effect: "Allow",
			Action: []string{"cloudformation:DescribeStacks",
				"cloudformation:DescribeStackResource"},
			Resource: gocf.Join("", cloudFormationThisStackArn...),
		},
	},
Example #5
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 #6
0
func (converter *templateConverter) results() (*gocf.StringExpr, error) {
	if nil != converter.conversionError {
		return nil, converter.conversionError
	}
	return gocf.Join("", converter.contents...), nil
}
Example #7
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 #8
0
// BucketArnAllKeys returns an Arn value that can be used
// lambdaFn.RoleDefinition.Privileges `Resource` value.  It includes
// the trailing `/*` wildcard to support item acccess
func (storage *MessageBodyStorage) BucketArnAllKeys() *gocf.StringExpr {
	return gocf.Join("",
		gocf.String("arn:aws:s3:::"),
		storage.bucketNameExpr,
		gocf.String("/*"))
}
Example #9
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 #10
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 #11
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 #12
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 #13
0
			Action: []string{"logs:CreateLogGroup",
				"logs:CreateLogStream",
				"logs:PutLogEvents"},
			Effect:   "Allow",
			Resource: gocf.String("arn:aws:logs:*:*:*"),
		},
		iamPolicyStatement{
			Action:   []string{"cloudwatch:PutMetricData"},
			Effect:   "Allow",
			Resource: wildcardArn,
		},
		iamPolicyStatement{
			Effect: "Allow",
			Action: []string{"cloudformation:DescribeStacks",
				"cloudformation:DescribeStackResource"},
			Resource: gocf.Join("", cloudFormationThisStackArn...),
		},
	},
	"dynamodb": []iamPolicyStatement{
		iamPolicyStatement{
			Effect: "Allow",
			Action: []string{"dynamodb:DescribeStream",
				"dynamodb:GetRecords",
				"dynamodb:GetShardIterator",
				"dynamodb:ListStreams",
			},
		}},
	"kinesis": []iamPolicyStatement{
		iamPolicyStatement{
			Effect: "Allow",
			Action: []string{"kinesis:GetRecords",
Example #14
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
}