github.com/mweagle/Sparta@v1.15.0/aws/cloudformation/resources/zipToS3BucketResource.go (about) 1 package resources 2 3 import ( 4 "archive/zip" 5 "bytes" 6 "encoding/json" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "mime" 11 "os" 12 "path" 13 "strings" 14 15 "github.com/aws/aws-sdk-go/aws" 16 "github.com/aws/aws-sdk-go/aws/session" 17 "github.com/aws/aws-sdk-go/service/s3" 18 gocf "github.com/mweagle/go-cloudformation" 19 "github.com/pkg/errors" 20 "github.com/sirupsen/logrus" 21 ) 22 23 // DefaultManifestName is the name of the file that will be created 24 // at the root of the S3 bucket with user-supplied metadata 25 const DefaultManifestName = "MANIFEST.json" 26 27 // ZipToS3BucketResourceRequest is the data request made to a ZipToS3BucketResource 28 // lambda handler 29 type ZipToS3BucketResourceRequest struct { 30 SrcBucket *gocf.StringExpr 31 SrcKeyName *gocf.StringExpr 32 DestBucket *gocf.StringExpr 33 ManifestName string 34 Manifest map[string]interface{} 35 } 36 37 // ZipToS3BucketResource manages populating an S3 bucket with the contents 38 // of a ZIP file... 39 type ZipToS3BucketResource struct { 40 gocf.CloudFormationCustomResource 41 ZipToS3BucketResourceRequest 42 } 43 44 func (command ZipToS3BucketResource) unzip(session *session.Session, 45 event *CloudFormationLambdaEvent, 46 logger *logrus.Logger) (map[string]interface{}, error) { 47 48 unmarshalErr := json.Unmarshal(event.ResourceProperties, &command) 49 if unmarshalErr != nil { 50 return nil, unmarshalErr 51 } 52 53 // Fetch the ZIP contents and unpack them to the S3 bucket 54 svc := s3.New(session) 55 s3Object, s3ObjectErr := svc.GetObject(&s3.GetObjectInput{ 56 Bucket: aws.String(command.SrcBucket.Literal), 57 Key: aws.String(command.SrcKeyName.Literal), 58 }) 59 if nil != s3ObjectErr { 60 return nil, s3ObjectErr 61 } 62 // Put all the ZIP contents to the bucket 63 defer s3Object.Body.Close() 64 destFile, destFileErr := ioutil.TempFile("", "s3") 65 if nil != destFileErr { 66 return nil, destFileErr 67 } 68 defer os.Remove(destFile.Name()) 69 70 _, copyErr := io.Copy(destFile, s3Object.Body) 71 if nil != copyErr { 72 return nil, copyErr 73 } 74 zipReader, zipErr := zip.OpenReader(destFile.Name()) 75 if nil != zipErr { 76 return nil, zipErr 77 } 78 // Iterate through the files in the archive, 79 // printing some of their contents. 80 // TODO - refactor to a worker pool 81 totalFiles := 0 82 for _, eachFile := range zipReader.File { 83 totalFiles++ 84 85 stream, streamErr := eachFile.Open() 86 if nil != streamErr { 87 return nil, streamErr 88 } 89 bodySource, bodySourceErr := ioutil.ReadAll(stream) 90 if nil != bodySourceErr { 91 return nil, bodySourceErr 92 } 93 normalizedName := strings.TrimLeft(eachFile.Name, "/") 94 // Mime type? 95 fileExtension := path.Ext(eachFile.Name) 96 mimeType := mime.TypeByExtension(fileExtension) 97 if mimeType == "" { 98 mimeType = "application/octet-stream" 99 } 100 101 if len(normalizedName) > 0 { 102 s3PutObject := &s3.PutObjectInput{ 103 Body: bytes.NewReader(bodySource), 104 Bucket: aws.String(command.DestBucket.Literal), 105 Key: aws.String(fmt.Sprintf("/%s", eachFile.Name)), 106 ContentType: aws.String(mimeType), 107 } 108 _, err := svc.PutObject(s3PutObject) 109 if err != nil { 110 return nil, err 111 } 112 } 113 errClose := stream.Close() 114 if errClose != nil { 115 return nil, errors.Wrapf(errClose, "Failed to close S3 PutObject stream") 116 } 117 } 118 // Need to add the manifest data iff defined 119 if nil != command.Manifest { 120 manifestBytes, manifestErr := json.Marshal(command.Manifest) 121 if nil != manifestErr { 122 return nil, manifestErr 123 } 124 name := command.ManifestName 125 if name == "" { 126 name = DefaultManifestName 127 } 128 s3PutObject := &s3.PutObjectInput{ 129 Body: bytes.NewReader(manifestBytes), 130 Bucket: aws.String(command.DestBucket.Literal), 131 Key: aws.String(name), 132 ContentType: aws.String("application/json"), 133 } 134 _, err := svc.PutObject(s3PutObject) 135 if err != nil { 136 return nil, err 137 } 138 } 139 // Log some information 140 logger.WithFields(logrus.Fields{ 141 "TotalFileCount": totalFiles, 142 "ArchiveSize": *s3Object.ContentLength, 143 "S3Bucket": command.DestBucket, 144 }).Info("Expanded ZIP archive") 145 146 // All good 147 return nil, nil 148 } 149 150 // IAMPrivileges returns the IAM privs for this custom action 151 func (command *ZipToS3BucketResource) IAMPrivileges() []string { 152 // Empty implementation - s3Site.go handles setting up the IAM privs for this. 153 return []string{} 154 } 155 156 // Create implements the custom resource create operation 157 func (command ZipToS3BucketResource) Create(awsSession *session.Session, 158 event *CloudFormationLambdaEvent, 159 logger *logrus.Logger) (map[string]interface{}, error) { 160 return command.unzip(awsSession, event, logger) 161 } 162 163 // Update implements the custom resource update operation 164 func (command ZipToS3BucketResource) Update(awsSession *session.Session, 165 event *CloudFormationLambdaEvent, 166 logger *logrus.Logger) (map[string]interface{}, error) { 167 return command.unzip(awsSession, event, logger) 168 } 169 170 // Delete implements the custom resource delete operation 171 func (command ZipToS3BucketResource) Delete(awsSession *session.Session, 172 event *CloudFormationLambdaEvent, 173 logger *logrus.Logger) (map[string]interface{}, error) { 174 175 unmarshalErr := json.Unmarshal(event.ResourceProperties, &command) 176 if unmarshalErr != nil { 177 return nil, unmarshalErr 178 } 179 180 // Remove all objects from the bucket 181 totalItemsDeleted := 0 182 svc := s3.New(awsSession) 183 deleteItemsHandler := func(objectOutputs *s3.ListObjectsOutput, lastPage bool) bool { 184 params := &s3.DeleteObjectsInput{ 185 Bucket: aws.String(command.DestBucket.Literal), 186 Delete: &s3.Delete{ // Required 187 Objects: []*s3.ObjectIdentifier{}, 188 Quiet: aws.Bool(true), 189 }, 190 } 191 for _, eachObject := range objectOutputs.Contents { 192 totalItemsDeleted++ 193 params.Delete.Objects = append(params.Delete.Objects, &s3.ObjectIdentifier{ 194 Key: eachObject.Key, 195 }) 196 } 197 _, deleteResultErr := svc.DeleteObjects(params) 198 return nil == deleteResultErr 199 } 200 201 // Walk the bucket and cleanup... 202 params := &s3.ListObjectsInput{ 203 Bucket: aws.String(command.DestBucket.Literal), 204 MaxKeys: aws.Int64(1000), 205 } 206 err := svc.ListObjectsPages(params, deleteItemsHandler) 207 if nil != err { 208 return nil, err 209 } 210 211 // Cleanup the Manifest iff defined 212 var deleteErr error 213 if nil != command.Manifest { 214 name := command.ManifestName 215 if name == "" { 216 name = DefaultManifestName 217 } 218 manifestDeleteParams := &s3.DeleteObjectInput{ 219 Bucket: aws.String(command.DestBucket.Literal), 220 Key: aws.String(name), 221 } 222 _, deleteErr = svc.DeleteObject(manifestDeleteParams) 223 logger.WithFields(logrus.Fields{ 224 "TotalDeletedCount": totalItemsDeleted, 225 "S3Bucket": command.DestBucket, 226 }).Info("Purged S3 Bucket") 227 } 228 return nil, deleteErr 229 }