// export marshals the API data to a CloudFormation compatible representation func (api *API) export(serviceName string, session *session.Session, S3Bucket string, S3Key string, roleNameMap map[string]*gocf.StringExpr, template *gocf.Template, noop bool, logger *logrus.Logger) error { apiGatewayResourceNameForPath := func(fullPath string) string { pathParts := strings.Split(fullPath, "/") return CloudFormationResourceName("%sResource", pathParts[0], fullPath) } apiGatewayResName := CloudFormationResourceName("APIGateway", api.name) // Create an API gateway entry apiGatewayRes := &gocf.ApiGatewayRestApi{ Description: gocf.String(api.Description), FailOnWarnings: gocf.Bool(false), Name: gocf.String(api.name), } if "" != api.CloneFrom { apiGatewayRes.CloneFrom = gocf.String(api.CloneFrom) } if "" == api.Description { apiGatewayRes.Description = gocf.String(fmt.Sprintf("%s RestApi", serviceName)) } else { apiGatewayRes.Description = gocf.String(api.Description) } template.AddResource(apiGatewayResName, apiGatewayRes) apiGatewayRestAPIID := gocf.Ref(apiGatewayResName) // List of all the method resources we're creating s.t. the // deployment can DependOn them optionsMethodPathMap := make(map[string]bool) var apiMethodCloudFormationResources []string for eachResourceMethodKey, eachResourceDef := range api.resources { // First walk all the user resources and create intermediate paths // to repreesent all the resources var parentResource *gocf.StringExpr pathParts := strings.Split(strings.TrimLeft(eachResourceDef.pathPart, "/"), "/") pathAccumulator := []string{"/"} for index, eachPathPart := range pathParts { pathAccumulator = append(pathAccumulator, eachPathPart) resourcePathName := apiGatewayResourceNameForPath(strings.Join(pathAccumulator, "/")) if _, exists := template.Resources[resourcePathName]; !exists { cfResource := &gocf.ApiGatewayResource{ RestApiId: apiGatewayRestAPIID.String(), PathPart: gocf.String(eachPathPart), } if index <= 0 { cfResource.ParentId = gocf.GetAtt(apiGatewayResName, "RootResourceId") } else { cfResource.ParentId = parentResource } template.AddResource(resourcePathName, cfResource) } parentResource = gocf.Ref(resourcePathName).String() } // Add the lambda permission apiGatewayPermissionResourceName := CloudFormationResourceName("APIGatewayLambdaPerm", eachResourceMethodKey) lambdaInvokePermission := &gocf.LambdaPermission{ Action: gocf.String("lambda:InvokeFunction"), FunctionName: gocf.GetAtt(eachResourceDef.parentLambda.logicalName(), "Arn"), Principal: gocf.String(APIGatewayPrincipal), } template.AddResource(apiGatewayPermissionResourceName, lambdaInvokePermission) // BEGIN CORS - OPTIONS verb // CORS is API global, but it's possible that there are multiple different lambda functions // that are handling the same HTTP resource. In this case, track whether we've already created an // OPTIONS entry for this path and only append iff this is the first time through if api.CORSEnabled { methodResourceName := CloudFormationResourceName(fmt.Sprintf("%s-OPTIONS", eachResourceDef.pathPart), eachResourceDef.pathPart) _, resourceExists := optionsMethodPathMap[methodResourceName] if !resourceExists { template.AddResource(methodResourceName, corsOptionsGatewayMethod(apiGatewayRestAPIID, parentResource)) apiMethodCloudFormationResources = append(apiMethodCloudFormationResources, methodResourceName) optionsMethodPathMap[methodResourceName] = true } } // END CORS - OPTIONS verb // BEGIN - user defined verbs for eachMethodName, eachMethodDef := range eachResourceDef.Methods { apiGatewayMethod := &gocf.ApiGatewayMethod{ HttpMethod: gocf.String(eachMethodName), AuthorizationType: gocf.String("NONE"), ResourceId: parentResource.String(), RestApiId: apiGatewayRestAPIID.String(), Integration: &gocf.APIGatewayMethodIntegration{ IntegrationHttpMethod: gocf.String("POST"), Type: gocf.String("AWS"), RequestTemplates: defaultRequestTemplates(), Uri: gocf.Join("", gocf.String("arn:aws:apigateway:"), gocf.Ref("AWS::Region"), gocf.String(":lambda:path/2015-03-31/functions/"), gocf.GetAtt(eachResourceDef.parentLambda.logicalName(), "Arn"), gocf.String("/invocations")), }, } if len(eachMethodDef.Parameters) != 0 { requestParams := make(map[string]string, 0) for eachKey, eachBool := range eachMethodDef.Parameters { requestParams[eachKey] = fmt.Sprintf("%t", eachBool) } apiGatewayMethod.RequestParameters = requestParams } // Add the integration response RegExps apiGatewayMethod.Integration.IntegrationResponses = integrationResponses(eachMethodDef.Integration.Responses, api.CORSEnabled) // Add outbound method responses apiGatewayMethod.MethodResponses = methodResponses(eachMethodDef.Responses, api.CORSEnabled) prefix := fmt.Sprintf("%s%s", eachMethodDef.httpMethod, eachResourceMethodKey) methodResourceName := CloudFormationResourceName(prefix, eachResourceMethodKey, serviceName) res := template.AddResource(methodResourceName, apiGatewayMethod) res.DependsOn = append(res.DependsOn, apiGatewayPermissionResourceName) apiMethodCloudFormationResources = append(apiMethodCloudFormationResources, methodResourceName) } } // END if nil != api.stage { // Is the stack already deployed? stageName := api.stage.name stageInfo, stageInfoErr := apiStageInfo(api.name, stageName, session, noop, logger) if nil != stageInfoErr { return stageInfoErr } if nil == stageInfo { // Use a stable identifier so that we can update the existing deployment apiDeploymentResName := CloudFormationResourceName("APIGatewayDeployment", serviceName) apiDeployment := &gocf.ApiGatewayDeployment{ Description: gocf.String(api.stage.Description), RestApiId: apiGatewayRestAPIID.String(), StageName: gocf.String(stageName), StageDescription: &gocf.APIGatewayDeploymentStageDescription{ StageName: gocf.String(api.stage.name), Description: gocf.String(api.stage.Description), Variables: api.stage.Variables, }, } if api.stage.CacheClusterEnabled { apiDeployment.StageDescription.CacheClusterEnabled = gocf.Bool(api.stage.CacheClusterEnabled) } if api.stage.CacheClusterSize != "" { apiDeployment.StageDescription.CacheClusterSize = gocf.String(api.stage.CacheClusterSize) } deployment := template.AddResource(apiDeploymentResName, apiDeployment) deployment.DependsOn = append(deployment.DependsOn, apiMethodCloudFormationResources...) deployment.DependsOn = append(deployment.DependsOn, apiGatewayResName) } else { newDeployment := &gocf.ApiGatewayDeployment{ Description: gocf.String("Sparta deploy"), RestApiId: apiGatewayRestAPIID.String(), StageName: gocf.String(stageName), } // Use an unstable ID s.t. we can actually create a new deployment event. Not sure how this // is going to work with deletes... deploymentResName := CloudFormationResourceName("APIGatewayDeployment") deployment := template.AddResource(deploymentResName, newDeployment) deployment.DependsOn = append(deployment.DependsOn, apiMethodCloudFormationResources...) deployment.DependsOn = append(deployment.DependsOn, apiGatewayResName) } template.Outputs[OutputAPIGatewayURL] = &gocf.Output{ Description: "API Gateway URL", Value: gocf.Join("", gocf.String("https://"), apiGatewayRestAPIID, gocf.String(".execute-api."), gocf.Ref("AWS::Region"), gocf.String(".amazonaws.com/"), gocf.String(stageName)), } } return nil }
// ensureIAMRoleForCustomResource ensures that the single IAM::Role for a single // AWS principal (eg, s3.*.*) exists, and includes statements for the given // sourceArn. Sparta uses a single IAM::Role for the CustomResource configuration // lambda, which is the union of all Arns in the application. func ensureIAMRoleForCustomResource(awsPrincipalName string, sourceArn *gocf.StringExpr, template *gocf.Template, logger *logrus.Logger) (string, error) { var principalActions []string switch awsPrincipalName { case cloudformationresources.SNSLambdaEventSource: principalActions = PushSourceConfigurationActions.SNSLambdaEventSource case cloudformationresources.S3LambdaEventSource: principalActions = PushSourceConfigurationActions.S3LambdaEventSource case cloudformationresources.SESLambdaEventSource: principalActions = PushSourceConfigurationActions.SESLambdaEventSource case cloudformationresources.CloudWatchLogsLambdaEventSource: principalActions = PushSourceConfigurationActions.CloudWatchLogsLambdaEventSource default: return "", fmt.Errorf("Unsupported principal for IAM role creation: %s", awsPrincipalName) } // What's the stable IAMRoleName? resourceBaseName := fmt.Sprintf("CustomResource%sIAMRole", awsPrincipalToService(awsPrincipalName)) stableRoleName := CloudFormationResourceName(resourceBaseName, awsPrincipalName) // Ensure it exists, then check to see if this Source ARN is already specified... // Checking equality with Stringable? // Create a new Role var existingIAMRole *gocf.IAMRole existingResource, exists := template.Resources[stableRoleName] logger.WithFields(logrus.Fields{ "PrincipalActions": principalActions, "SourceArn": sourceArn, }).Debug("Ensuring IAM Role results") if !exists { // Insert the IAM role here. We'll walk the policies data in the next section // to make sure that the sourceARN we have is in the list statements := CommonIAMStatements.Core iamPolicyList := gocf.IAMPoliciesList{} iamPolicyList = append(iamPolicyList, gocf.IAMPolicies{ PolicyDocument: ArbitraryJSONObject{ "Version": "2012-10-17", "Statement": statements, }, PolicyName: gocf.String(fmt.Sprintf("%sPolicy", stableRoleName)), }, ) existingIAMRole = &gocf.IAMRole{ AssumeRolePolicyDocument: AssumePolicyDocument, Policies: &iamPolicyList, } template.AddResource(stableRoleName, existingIAMRole) // Create a new IAM Role resource logger.WithFields(logrus.Fields{ "RoleName": stableRoleName, }).Debug("Inserting IAM Role") } else { existingIAMRole = existingResource.Properties.(*gocf.IAMRole) } // Walk the existing statements if nil != existingIAMRole.Policies { for _, eachPolicy := range *existingIAMRole.Policies { policyDoc := eachPolicy.PolicyDocument.(ArbitraryJSONObject) statements := policyDoc["Statement"] for _, eachStatement := range statements.([]spartaIAM.PolicyStatement) { if sourceArn.String() == eachStatement.Resource.String() { logger.WithFields(logrus.Fields{ "RoleName": stableRoleName, "SourceArn": sourceArn.String(), }).Debug("SourceArn already exists for IAM Policy") return stableRoleName, nil } } } logger.WithFields(logrus.Fields{ "RoleName": stableRoleName, "Action": principalActions, "Resource": sourceArn, }).Debug("Inserting Actions for configuration ARN") // Add this statement to the first policy, iff the actions are non-empty if len(principalActions) > 0 { rootPolicy := (*existingIAMRole.Policies)[0] rootPolicyDoc := rootPolicy.PolicyDocument.(ArbitraryJSONObject) rootPolicyStatements := rootPolicyDoc["Statement"].([]spartaIAM.PolicyStatement) rootPolicyDoc["Statement"] = append(rootPolicyStatements, spartaIAM.PolicyStatement{ Effect: "Allow", Action: principalActions, Resource: sourceArn, }) } return stableRoleName, nil } return "", fmt.Errorf("Unable to find Policies entry for IAM role: %s", stableRoleName) }