github.com/mweagle/Sparta@v1.15.0/s3site_build.go (about) 1 // +build !lambdabinary 2 3 package sparta 4 5 import ( 6 "github.com/aws/aws-sdk-go/aws" 7 "github.com/aws/aws-sdk-go/service/s3" 8 cfCustomResources "github.com/mweagle/Sparta/aws/cloudformation/resources" 9 spartaIAM "github.com/mweagle/Sparta/aws/iam" 10 gocf "github.com/mweagle/go-cloudformation" 11 "github.com/pkg/errors" 12 "github.com/sirupsen/logrus" 13 ) 14 15 const ( 16 // OutputS3SiteURL is the keyname used in the CloudFormation Output 17 // that stores the S3 backed static site provisioned with this Sparta application 18 // @enum OutputKey 19 OutputS3SiteURL = "S3SiteURL" 20 ) 21 22 // Create the resource, which will be part of the stack definition and use a CustomResource 23 // to copy the content. Which means we need PutItem access to the target Bucket. Use 24 // Cloudformation to create a random bucketname: 25 // http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket.html 26 27 // Need to create the S3 target bucket 28 // http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#putBucketWebsite-property 29 30 // export marshals the API data to a CloudFormation compatible representation 31 func (s3Site *S3Site) export(serviceName string, 32 binaryName string, 33 S3Bucket string, 34 S3Key string, 35 S3ResourcesKey string, 36 apiGatewayOutputs map[string]*gocf.Output, 37 roleNameMap map[string]*gocf.StringExpr, 38 template *gocf.Template, 39 logger *logrus.Logger) error { 40 41 if s3Site.WebsiteConfiguration == nil { 42 s3Site.WebsiteConfiguration = &s3.WebsiteConfiguration{ 43 ErrorDocument: &s3.ErrorDocument{ 44 Key: aws.String("error.html"), 45 }, 46 IndexDocument: &s3.IndexDocument{ 47 Suffix: aws.String("index.html"), 48 }, 49 } 50 } 51 // Ensure everything is set 52 if s3Site.WebsiteConfiguration.ErrorDocument == nil { 53 s3Site.WebsiteConfiguration.ErrorDocument = &s3.ErrorDocument{ 54 Key: aws.String("error.html"), 55 } 56 } 57 if s3Site.WebsiteConfiguration.IndexDocument == nil { 58 s3Site.WebsiteConfiguration.IndexDocument = &s3.IndexDocument{ 59 Suffix: aws.String("index.html"), 60 } 61 } 62 63 ////////////////////////////////////////////////////////////////////////////// 64 // 1 - Create the S3 bucket. The "BucketName" property is empty s.t. 65 // AWS will assign a unique one. 66 67 s3WebsiteConfig := &gocf.S3BucketWebsiteConfiguration{ 68 ErrorDocument: gocf.String(aws.StringValue(s3Site.WebsiteConfiguration.ErrorDocument.Key)), 69 IndexDocument: gocf.String(aws.StringValue(s3Site.WebsiteConfiguration.IndexDocument.Suffix)), 70 } 71 s3Bucket := &gocf.S3Bucket{ 72 AccessControl: gocf.String("PublicRead"), 73 WebsiteConfiguration: s3WebsiteConfig, 74 } 75 if s3Site.BucketName != nil { 76 s3Bucket.BucketName = s3Site.BucketName 77 } 78 s3BucketResourceName := s3Site.CloudFormationS3ResourceName() 79 cfResource := template.AddResource(s3BucketResourceName, s3Bucket) 80 cfResource.DeletionPolicy = "Delete" 81 82 template.Outputs[OutputS3SiteURL] = &gocf.Output{ 83 Description: "S3 Website URL", 84 Value: gocf.GetAtt(s3BucketResourceName, "WebsiteURL"), 85 } 86 87 // Represents the S3 ARN that is provisioned 88 s3SiteBucketResourceValue := gocf.Join("", 89 gocf.String("arn:aws:s3:::"), 90 gocf.Ref(s3BucketResourceName)) 91 s3SiteBucketAllKeysResourceValue := gocf.Join("", 92 gocf.String("arn:aws:s3:::"), 93 gocf.Ref(s3BucketResourceName), 94 gocf.String("/*")) 95 96 ////////////////////////////////////////////////////////////////////////////// 97 // 2 - Add a bucket policy to enable anonymous access, as the PublicRead 98 // canned ACL doesn't seem to do what is implied. 99 // TODO - determine if this is needed or if PublicRead is being misued 100 s3SiteBucketPolicy := &gocf.S3BucketPolicy{ 101 Bucket: gocf.Ref(s3BucketResourceName).String(), 102 PolicyDocument: ArbitraryJSONObject{ 103 "Version": "2012-10-17", 104 "Statement": []ArbitraryJSONObject{ 105 { 106 "Sid": "PublicReadGetObject", 107 "Effect": "Allow", 108 "Principal": ArbitraryJSONObject{ 109 "AWS": "*", 110 }, 111 "Action": "s3:GetObject", 112 "Resource": s3SiteBucketAllKeysResourceValue, 113 }, 114 }, 115 }, 116 } 117 s3BucketPolicyResourceName := stableCloudformationResourceName("S3SiteBucketPolicy") 118 template.AddResource(s3BucketPolicyResourceName, s3SiteBucketPolicy) 119 120 ////////////////////////////////////////////////////////////////////////////// 121 // 3 - Create the IAM role for the lambda function 122 // The lambda function needs to download the posted resource content, as well 123 // as manage the S3 bucket that hosts the site. 124 statements := CommonIAMStatements.Core 125 statements = append(statements, spartaIAM.PolicyStatement{ 126 Action: []string{"s3:ListBucket", 127 "s3:ListObjectsPages"}, 128 Effect: "Allow", 129 Resource: s3SiteBucketResourceValue, 130 }) 131 statements = append(statements, spartaIAM.PolicyStatement{ 132 Action: []string{"s3:DeleteObject", 133 "s3:PutObject", 134 "s3:DeleteObjects"}, 135 Effect: "Allow", 136 Resource: s3SiteBucketAllKeysResourceValue, 137 }) 138 statements = append(statements, spartaIAM.PolicyStatement{ 139 Action: []string{"s3:GetObject"}, 140 Effect: "Allow", 141 Resource: gocf.Join("", 142 gocf.String("arn:aws:s3:::"), 143 gocf.String(S3Bucket), 144 gocf.String("/"), 145 gocf.String(S3ResourcesKey)), 146 }) 147 148 iamPolicyList := gocf.IAMRolePolicyList{} 149 iamPolicyList = append(iamPolicyList, 150 gocf.IAMRolePolicy{ 151 PolicyDocument: ArbitraryJSONObject{ 152 "Version": "2012-10-17", 153 "Statement": statements, 154 }, 155 PolicyName: gocf.String("S3SiteMgmnt"), 156 }, 157 ) 158 159 iamS3Role := &gocf.IAMRole{ 160 AssumeRolePolicyDocument: AssumePolicyDocument, 161 Policies: &iamPolicyList, 162 } 163 164 iamRoleName := stableCloudformationResourceName("S3SiteIAMRole") 165 cfResource = template.AddResource(iamRoleName, iamS3Role) 166 cfResource.DependsOn = append(cfResource.DependsOn, s3BucketResourceName) 167 iamRoleRef := gocf.GetAtt(iamRoleName, "Arn") 168 169 // Create the IAM role and CustomAction handler to do the work 170 171 ////////////////////////////////////////////////////////////////////////////// 172 // 4 - Create the lambda function definition that executes with the 173 // dynamically provisioned IAM policy. This is similar to what happens in 174 // EnsureCustomResourceHandler, but due to the more complex IAM rules 175 // there's a bit of duplication 176 // handlerName := lambdaExportNameForCustomResourceType(cloudformationresources.ZipToS3Bucket) 177 logger.WithFields(logrus.Fields{ 178 "CustomResourceType": cfCustomResources.ZipToS3Bucket, 179 }).Debug("Sparta CloudFormation custom resource handler info") 180 181 // Since this is a custom resource command, stuff the type in the environment 182 userDispatchMap := map[string]*gocf.StringExpr{ 183 EnvVarCustomResourceTypeName: gocf.String(cfCustomResources.ZipToS3Bucket), 184 } 185 lambdaEnv, lambdaEnvErr := lambdaFunctionEnvironment(userDispatchMap, 186 cfCustomResources.ZipToS3Bucket, 187 nil, 188 logger) 189 if lambdaEnvErr != nil { 190 return errors.Wrapf(lambdaEnvErr, "Failed to create S3 site resource") 191 } 192 customResourceHandlerDef := gocf.LambdaFunction{ 193 Code: &gocf.LambdaFunctionCode{ 194 S3Bucket: gocf.String(S3Bucket), 195 S3Key: gocf.String(S3Key), 196 }, 197 Description: gocf.String(customResourceDescription(serviceName, 198 "S3 static site")), 199 Handler: gocf.String(binaryName), 200 Role: iamRoleRef, 201 Runtime: gocf.String(GoLambdaVersion), 202 MemorySize: gocf.Integer(256), 203 Timeout: gocf.Integer(180), 204 // Let AWS assign the function name 205 /* 206 FunctionName: lambdaFunctionName.String(), 207 */ 208 Environment: lambdaEnv, 209 } 210 lambdaResourceName := stableCloudformationResourceName("S3SiteCreator") 211 cfResource = template.AddResource(lambdaResourceName, customResourceHandlerDef) 212 cfResource.DependsOn = append(cfResource.DependsOn, 213 s3BucketResourceName, 214 iamRoleName) 215 216 ////////////////////////////////////////////////////////////////////////////// 217 // 5 - Create the custom resource that invokes the site bootstrapper lambda to 218 // actually populate the S3 with content 219 customResourceName := CloudFormationResourceName("S3SiteBuilder") 220 newResource, err := newCloudFormationResource(cfCustomResources.ZipToS3Bucket, logger) 221 if nil != err { 222 return errors.Wrapf(err, "Failed to create ZipToS3Bucket CustomResource") 223 } 224 zipResource, zipResourceOK := newResource.(*cfCustomResources.ZipToS3BucketResource) 225 if !zipResourceOK { 226 return errors.Errorf("Failed to type assert *cfCustomResources.ZipToS3BucketResource custom resource") 227 } 228 zipResource.ServiceToken = gocf.GetAtt(lambdaResourceName, "Arn") 229 zipResource.SrcKeyName = gocf.String(S3ResourcesKey) 230 zipResource.SrcBucket = gocf.String(S3Bucket) 231 zipResource.DestBucket = gocf.Ref(s3BucketResourceName).String() 232 233 // Build the manifest data with any output info... 234 manifestData := make(map[string]interface{}) 235 for eachKey, eachOutput := range apiGatewayOutputs { 236 manifestData[eachKey] = map[string]interface{}{ 237 "Description": eachOutput.Description, 238 "Value": eachOutput.Value, 239 } 240 } 241 if len(s3Site.UserManifestData) != 0 { 242 manifestData["userdata"] = s3Site.UserManifestData 243 } 244 245 zipResource.Manifest = manifestData 246 cfResource = template.AddResource(customResourceName, zipResource) 247 cfResource.DependsOn = append(cfResource.DependsOn, 248 lambdaResourceName, 249 s3BucketResourceName) 250 251 return nil 252 } 253 254 // NewS3Site returns a new S3Site pointer initialized with the 255 // static resources at the supplied path. If resources is a directory, 256 // the contents will be recursively archived and used to populate 257 // the new S3 bucket. 258 func NewS3Site(resources string) (*S3Site, error) { 259 // We'll ensure its valid during the build step, since 260 // there could be a go:generate command in the source that 261 // actually builds it. 262 site := &S3Site{ 263 resources: resources, 264 UserManifestData: map[string]interface{}{}, 265 } 266 return site, nil 267 }