github.com/mweagle/Sparta@v1.15.0/provision.go (about) 1 package sparta 2 3 import ( 4 "bytes" 5 "fmt" 6 "reflect" 7 "text/template" 8 9 spartaCF "github.com/mweagle/Sparta/aws/cloudformation" 10 cfCustomResources "github.com/mweagle/Sparta/aws/cloudformation/resources" 11 spartaIAM "github.com/mweagle/Sparta/aws/iam" 12 gocf "github.com/mweagle/go-cloudformation" 13 "github.com/pkg/errors" 14 "github.com/sirupsen/logrus" 15 ) 16 17 const ( 18 // ScratchDirectory is the cwd relative path component 19 // where intermediate build artifacts are created 20 ScratchDirectory = ".sparta" 21 // EnvVarCustomResourceTypeName is the environment variable 22 // name that stores the CustomResource TypeName that should be 23 // instantiated 24 EnvVarCustomResourceTypeName = "SPARTA_CUSTOM_RESOURCE_TYPE" 25 ) 26 27 // This is a literal version of the DiscoveryInfo struct. 28 var discoveryData = ` 29 { 30 "ResourceID": "<< .TagLogicalResourceID >>", 31 "Region": "{"Ref" : "AWS::Region"}", 32 "StackID": "{"Ref" : "AWS::StackId"}", 33 "StackName": "{"Ref" : "AWS::StackName"}", 34 "Resources":{<<range $eachDepResource, $eachOutputString := .Resources>> 35 "<< $eachDepResource >>" : << $eachOutputString >><< trailingComma >><<end>> 36 } 37 }` 38 39 // 40 type discoveryDataTemplateData struct { 41 TagLogicalResourceID string 42 Resources map[string]string 43 } 44 45 func lambdaFunctionEnvironment(userEnvMap map[string]*gocf.StringExpr, 46 resourceID string, 47 deps map[string]string, 48 logger *logrus.Logger) (*gocf.LambdaFunctionEnvironment, error) { 49 // Merge everything, add the deps 50 envMap := make(map[string]interface{}) 51 for eachKey, eachValue := range userEnvMap { 52 envMap[eachKey] = eachValue 53 } 54 discoveryInfo, discoveryInfoErr := discoveryInfoForResource(resourceID, deps) 55 if discoveryInfoErr != nil { 56 return nil, errors.Wrapf(discoveryInfoErr, "Failed to calculate dependency info") 57 } 58 envMap[envVarLogLevel] = logger.Level.String() 59 envMap[envVarDiscoveryInformation] = discoveryInfo 60 return &gocf.LambdaFunctionEnvironment{ 61 Variables: envMap, 62 }, nil 63 } 64 65 func discoveryInfoForResource(resID string, deps map[string]string) (*gocf.StringExpr, error) { 66 discoveryDataTemplateData := &discoveryDataTemplateData{ 67 TagLogicalResourceID: resID, 68 Resources: deps, 69 } 70 totalDeps := len(deps) 71 var templateFuncMap = template.FuncMap{ 72 // The name "inc" is what the function will be called in the template text. 73 "trailingComma": func() string { 74 totalDeps-- 75 if totalDeps > 0 { 76 return "," 77 } 78 return "" 79 }, 80 } 81 82 discoveryTemplate, discoveryTemplateErr := template.New("discoveryData"). 83 Delims("<<", ">>"). 84 Funcs(templateFuncMap). 85 Parse(discoveryData) 86 if nil != discoveryTemplateErr { 87 return nil, discoveryTemplateErr 88 } 89 90 var templateResults bytes.Buffer 91 evalResultErr := discoveryTemplate.Execute(&templateResults, discoveryDataTemplateData) 92 if nil != evalResultErr { 93 return nil, evalResultErr 94 } 95 templateReader := bytes.NewReader(templateResults.Bytes()) 96 templateExpr, templateExprErr := spartaCF.ConvertToTemplateExpression(templateReader, nil) 97 if templateExprErr != nil { 98 return nil, templateExprErr 99 } 100 return gocf.Base64(templateExpr), nil 101 } 102 103 // EnsureCustomResourceHandler handles ensuring that the custom resource responsible 104 // for supporting the operation is actually part of this stack. The returned 105 // string value is the CloudFormation resource name that implements this 106 // resource. The customResourceCloudFormationTypeName must have already 107 // been registered with gocf and implement the resources.CustomResourceCommand 108 // interface 109 func EnsureCustomResourceHandler(serviceName string, 110 customResourceCloudFormationTypeName string, 111 sourceArn *gocf.StringExpr, 112 dependsOn []string, 113 template *gocf.Template, 114 S3Bucket string, 115 S3Key string, 116 logger *logrus.Logger) (string, error) { 117 118 // Ok, we need a way to round trip this type as the AWS lambda function name. 119 // The problem with this is that the full CustomResource::Type value isn't an 120 // AWS Lambda friendly name. We want to do this so that in the AWS lambda handler 121 // we can attempt to instantiate a new CustomAction resource, typecast it to a 122 // CustomResourceCommand type and then apply the workflow. Doing this means 123 // we can decouple the lookup logic for custom resource... 124 125 resource := gocf.NewResourceByType(customResourceCloudFormationTypeName) 126 if resource == nil { 127 return "", errors.Errorf("Unable to create custom resource handler of type: %v", customResourceCloudFormationTypeName) 128 } 129 command, commandOk := resource.(cfCustomResources.CustomResourceCommand) 130 if !commandOk { 131 return "", errors.Errorf("Cannot type assert resource type %s to CustomResourceCommand", customResourceCloudFormationTypeName) 132 } 133 134 // Prefix 135 commandType := reflect.TypeOf(command) 136 customResourceTypeName := fmt.Sprintf("%T", command) 137 prefixName := fmt.Sprintf("%s-CFRes", serviceName) 138 subscriberHandlerName := CloudFormationResourceName(prefixName, customResourceTypeName) 139 140 ////////////////////////////////////////////////////////////////////////////// 141 // IAM Role definition 142 iamResourceName, err := ensureIAMRoleForCustomResource(command, 143 sourceArn, 144 template, 145 logger) 146 if nil != err { 147 return "", errors.Wrapf(err, 148 "Failed to ensure IAM Role for custom resource: %T", 149 command) 150 } 151 iamRoleRef := gocf.GetAtt(iamResourceName, "Arn") 152 _, exists := template.Resources[subscriberHandlerName] 153 if exists { 154 return subscriberHandlerName, nil 155 } 156 157 // Encode the resourceType... 158 configuratorDescription := customResourceDescription(serviceName, customResourceTypeName) 159 160 ////////////////////////////////////////////////////////////////////////////// 161 // Custom Resource Lambda Handler 162 // Insert it into the template resources... 163 logger.WithFields(logrus.Fields{ 164 "CloudFormationResourceType": customResourceCloudFormationTypeName, 165 "Resource": customResourceTypeName, 166 "TypeOf": commandType.String(), 167 }).Info("Including Lambda CustomResource") 168 169 // Don't forget the discovery info... 170 userDispatchMap := map[string]*gocf.StringExpr{ 171 EnvVarCustomResourceTypeName: gocf.String(customResourceCloudFormationTypeName), 172 } 173 lambdaEnv, lambdaEnvErr := lambdaFunctionEnvironment(userDispatchMap, 174 customResourceTypeName, 175 nil, 176 logger) 177 if lambdaEnvErr != nil { 178 return "", errors.Wrapf(lambdaEnvErr, "Failed to create environment for required custom resource") 179 } 180 // Add the special key that's the custom resource type name 181 customResourceHandlerDef := gocf.LambdaFunction{ 182 Code: &gocf.LambdaFunctionCode{ 183 S3Bucket: gocf.String(S3Bucket), 184 S3Key: gocf.String(S3Key), 185 }, 186 Runtime: gocf.String(GoLambdaVersion), 187 Description: gocf.String(configuratorDescription), 188 Handler: gocf.String(SpartaBinaryName), 189 Role: iamRoleRef, 190 Timeout: gocf.Integer(30), 191 // Let AWS assign a name here... 192 // FunctionName: lambdaFunctionName.String(), 193 // DISPATCH INFORMATION 194 Environment: lambdaEnv, 195 } 196 197 cfResource := template.AddResource(subscriberHandlerName, customResourceHandlerDef) 198 if nil != dependsOn && (len(dependsOn) > 0) { 199 cfResource.DependsOn = append(cfResource.DependsOn, dependsOn...) 200 } 201 return subscriberHandlerName, nil 202 } 203 204 // ensureIAMRoleForCustomResource ensures that the single IAM::Role for a single 205 // AWS principal (eg, s3.*.*) exists, and includes statements for the given 206 // sourceArn. Sparta uses a single IAM::Role for the CustomResource configuration 207 // lambda, which is the union of all Arns in the application. 208 func ensureIAMRoleForCustomResource(command cfCustomResources.CustomResourceCommand, 209 sourceArn *gocf.StringExpr, 210 template *gocf.Template, 211 logger *logrus.Logger) (string, error) { 212 213 // What's the stable IAMRoleName? 214 commandName := fmt.Sprintf("%T", command) 215 resourceBaseName := fmt.Sprintf("CFResIAMRole%s", commandName) 216 stableRoleName := CloudFormationResourceName(resourceBaseName, resourceBaseName) 217 218 // Is it a privileged command? 219 var privileges []string 220 privilegedCommand, privilegedCommandOk := command.(cfCustomResources.CustomResourcePrivilegedCommand) 221 if privilegedCommandOk { 222 privileges = privilegedCommand.IAMPrivileges() 223 } 224 225 // Ensure it exists, then check to see if this Source ARN is already specified... 226 // Checking equality with Stringable? 227 228 // Create a new Role 229 var existingIAMRole *gocf.IAMRole 230 existingResource, exists := template.Resources[stableRoleName] 231 logger.WithFields(logrus.Fields{ 232 "PrincipalActions": privileges, 233 "SourceArn": sourceArn, 234 }).Debug("Ensuring IAM Role results") 235 236 if !exists { 237 // Insert the IAM role here. We'll walk the policies data in the next section 238 // to make sure that the sourceARN we have is in the list 239 statements := CommonIAMStatements.Core 240 241 iamPolicyList := gocf.IAMRolePolicyList{} 242 iamPolicyList = append(iamPolicyList, 243 gocf.IAMRolePolicy{ 244 PolicyDocument: ArbitraryJSONObject{ 245 "Version": "2012-10-17", 246 "Statement": statements, 247 }, 248 PolicyName: gocf.String(fmt.Sprintf("%sPolicy", stableRoleName)), 249 }, 250 ) 251 252 existingIAMRole = &gocf.IAMRole{ 253 AssumeRolePolicyDocument: AssumePolicyDocument, 254 Policies: &iamPolicyList, 255 } 256 template.AddResource(stableRoleName, existingIAMRole) 257 258 // Create a new IAM Role resource 259 logger.WithFields(logrus.Fields{ 260 "RoleName": stableRoleName, 261 }).Debug("Inserting IAM Role") 262 } else { 263 existingIAMRole = existingResource.Properties.(*gocf.IAMRole) 264 } 265 266 // ARNs are only required if there are non-empty privileges associated 267 // with the command 268 if sourceArn == nil { 269 if len(privileges) != 0 { 270 return "", errors.Errorf("CustomResource %s requires a SourceARN to apply it's %d principle actions", 271 commandName, 272 len(privileges)) 273 } 274 return stableRoleName, nil 275 } 276 // Walk the existing statements 277 if nil != existingIAMRole.Policies { 278 for _, eachPolicy := range *existingIAMRole.Policies { 279 policyDoc := eachPolicy.PolicyDocument.(ArbitraryJSONObject) 280 statements := policyDoc["Statement"] 281 for _, eachStatement := range statements.([]spartaIAM.PolicyStatement) { 282 if sourceArn.String() == eachStatement.Resource.String() { 283 284 logger.WithFields(logrus.Fields{ 285 "RoleName": stableRoleName, 286 "SourceArn": sourceArn.String(), 287 }).Debug("SourceArn already exists for IAM Policy") 288 return stableRoleName, nil 289 } 290 } 291 } 292 293 logger.WithFields(logrus.Fields{ 294 "RoleName": stableRoleName, 295 "Action": privileges, 296 "Resource": sourceArn, 297 }).Debug("Inserting Actions for configuration ARN") 298 299 // Add this statement to the first policy, iff the actions are non-empty 300 if len(privileges) > 0 { 301 rootPolicy := (*existingIAMRole.Policies)[0] 302 rootPolicyDoc := rootPolicy.PolicyDocument.(ArbitraryJSONObject) 303 rootPolicyStatements := rootPolicyDoc["Statement"].([]spartaIAM.PolicyStatement) 304 rootPolicyDoc["Statement"] = append(rootPolicyStatements, 305 spartaIAM.PolicyStatement{ 306 Effect: "Allow", 307 Action: privileges, 308 Resource: sourceArn, 309 }) 310 } 311 return stableRoleName, nil 312 } 313 return "", errors.Errorf("Unable to find Policies entry for IAM role: %s", stableRoleName) 314 }