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  }