github.com/bradfeehan/terraform@v0.7.0-rc3.0.20170529055808-34b45c5ad841/builtin/providers/aws/resource_aws_s3_bucket_object.go (about)

     1  package aws
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"log"
     8  	"net/url"
     9  	"os"
    10  	"sort"
    11  	"strings"
    12  
    13  	"github.com/hashicorp/terraform/helper/schema"
    14  	"github.com/mitchellh/go-homedir"
    15  
    16  	"github.com/aws/aws-sdk-go/aws"
    17  	"github.com/aws/aws-sdk-go/aws/awserr"
    18  	"github.com/aws/aws-sdk-go/service/kms"
    19  	"github.com/aws/aws-sdk-go/service/s3"
    20  )
    21  
    22  func resourceAwsS3BucketObject() *schema.Resource {
    23  	return &schema.Resource{
    24  		Create: resourceAwsS3BucketObjectPut,
    25  		Read:   resourceAwsS3BucketObjectRead,
    26  		Update: resourceAwsS3BucketObjectPut,
    27  		Delete: resourceAwsS3BucketObjectDelete,
    28  
    29  		Schema: map[string]*schema.Schema{
    30  			"bucket": {
    31  				Type:     schema.TypeString,
    32  				Required: true,
    33  				ForceNew: true,
    34  			},
    35  
    36  			"acl": {
    37  				Type:         schema.TypeString,
    38  				Default:      "private",
    39  				Optional:     true,
    40  				ValidateFunc: validateS3BucketObjectAclType,
    41  			},
    42  
    43  			"cache_control": {
    44  				Type:     schema.TypeString,
    45  				Optional: true,
    46  			},
    47  
    48  			"content_disposition": {
    49  				Type:     schema.TypeString,
    50  				Optional: true,
    51  			},
    52  
    53  			"content_encoding": {
    54  				Type:     schema.TypeString,
    55  				Optional: true,
    56  			},
    57  
    58  			"content_language": {
    59  				Type:     schema.TypeString,
    60  				Optional: true,
    61  			},
    62  
    63  			"content_type": {
    64  				Type:     schema.TypeString,
    65  				Optional: true,
    66  				Computed: true,
    67  			},
    68  
    69  			"key": {
    70  				Type:     schema.TypeString,
    71  				Required: true,
    72  				ForceNew: true,
    73  			},
    74  
    75  			"source": {
    76  				Type:          schema.TypeString,
    77  				Optional:      true,
    78  				ConflictsWith: []string{"content"},
    79  			},
    80  
    81  			"content": {
    82  				Type:          schema.TypeString,
    83  				Optional:      true,
    84  				ConflictsWith: []string{"source"},
    85  			},
    86  
    87  			"storage_class": {
    88  				Type:         schema.TypeString,
    89  				Optional:     true,
    90  				Computed:     true,
    91  				ValidateFunc: validateS3BucketObjectStorageClassType,
    92  			},
    93  
    94  			"server_side_encryption": {
    95  				Type:         schema.TypeString,
    96  				Optional:     true,
    97  				ValidateFunc: validateS3BucketObjectServerSideEncryption,
    98  				Computed:     true,
    99  			},
   100  
   101  			"kms_key_id": {
   102  				Type:         schema.TypeString,
   103  				Optional:     true,
   104  				ValidateFunc: validateArn,
   105  			},
   106  
   107  			"etag": {
   108  				Type: schema.TypeString,
   109  				// This will conflict with SSE-C and SSE-KMS encryption and multi-part upload
   110  				// if/when it's actually implemented. The Etag then won't match raw-file MD5.
   111  				// See http://docs.aws.amazon.com/AmazonS3/latest/API/RESTCommonResponseHeaders.html
   112  				Optional:      true,
   113  				Computed:      true,
   114  				ConflictsWith: []string{"kms_key_id", "server_side_encryption"},
   115  			},
   116  
   117  			"version_id": {
   118  				Type:     schema.TypeString,
   119  				Computed: true,
   120  			},
   121  
   122  			"tags": tagsSchema(),
   123  		},
   124  	}
   125  }
   126  
   127  func resourceAwsS3BucketObjectPut(d *schema.ResourceData, meta interface{}) error {
   128  	s3conn := meta.(*AWSClient).s3conn
   129  
   130  	restricted := meta.(*AWSClient).IsGovCloud() || meta.(*AWSClient).IsChinaCloud()
   131  
   132  	var body io.ReadSeeker
   133  
   134  	if v, ok := d.GetOk("source"); ok {
   135  		source := v.(string)
   136  		path, err := homedir.Expand(source)
   137  		if err != nil {
   138  			return fmt.Errorf("Error expanding homedir in source (%s): %s", source, err)
   139  		}
   140  		file, err := os.Open(path)
   141  		if err != nil {
   142  			return fmt.Errorf("Error opening S3 bucket object source (%s): %s", source, err)
   143  		}
   144  
   145  		body = file
   146  	} else if v, ok := d.GetOk("content"); ok {
   147  		content := v.(string)
   148  		body = bytes.NewReader([]byte(content))
   149  	} else {
   150  		return fmt.Errorf("Must specify \"source\" or \"content\" field")
   151  	}
   152  
   153  	bucket := d.Get("bucket").(string)
   154  	key := d.Get("key").(string)
   155  
   156  	putInput := &s3.PutObjectInput{
   157  		Bucket: aws.String(bucket),
   158  		Key:    aws.String(key),
   159  		ACL:    aws.String(d.Get("acl").(string)),
   160  		Body:   body,
   161  	}
   162  
   163  	if v, ok := d.GetOk("storage_class"); ok {
   164  		putInput.StorageClass = aws.String(v.(string))
   165  	}
   166  
   167  	if v, ok := d.GetOk("cache_control"); ok {
   168  		putInput.CacheControl = aws.String(v.(string))
   169  	}
   170  
   171  	if v, ok := d.GetOk("content_type"); ok {
   172  		putInput.ContentType = aws.String(v.(string))
   173  	}
   174  
   175  	if v, ok := d.GetOk("content_encoding"); ok {
   176  		putInput.ContentEncoding = aws.String(v.(string))
   177  	}
   178  
   179  	if v, ok := d.GetOk("content_language"); ok {
   180  		putInput.ContentLanguage = aws.String(v.(string))
   181  	}
   182  
   183  	if v, ok := d.GetOk("content_disposition"); ok {
   184  		putInput.ContentDisposition = aws.String(v.(string))
   185  	}
   186  
   187  	if v, ok := d.GetOk("server_side_encryption"); ok {
   188  		putInput.ServerSideEncryption = aws.String(v.(string))
   189  	}
   190  
   191  	if v, ok := d.GetOk("kms_key_id"); ok {
   192  		putInput.SSEKMSKeyId = aws.String(v.(string))
   193  		putInput.ServerSideEncryption = aws.String(s3.ServerSideEncryptionAwsKms)
   194  	}
   195  
   196  	if v, ok := d.GetOk("tags"); ok {
   197  		if restricted {
   198  			return fmt.Errorf("This region does not allow for tags on S3 objects")
   199  		}
   200  
   201  		// The tag-set must be encoded as URL Query parameters.
   202  		values := url.Values{}
   203  		for k, v := range v.(map[string]interface{}) {
   204  			values.Add(k, v.(string))
   205  		}
   206  		putInput.Tagging = aws.String(values.Encode())
   207  	}
   208  
   209  	resp, err := s3conn.PutObject(putInput)
   210  	if err != nil {
   211  		return fmt.Errorf("Error putting object in S3 bucket (%s): %s", bucket, err)
   212  	}
   213  
   214  	// See https://forums.aws.amazon.com/thread.jspa?threadID=44003
   215  	d.Set("etag", strings.Trim(*resp.ETag, `"`))
   216  
   217  	d.Set("version_id", resp.VersionId)
   218  	d.SetId(key)
   219  	return resourceAwsS3BucketObjectRead(d, meta)
   220  }
   221  
   222  func resourceAwsS3BucketObjectRead(d *schema.ResourceData, meta interface{}) error {
   223  	s3conn := meta.(*AWSClient).s3conn
   224  
   225  	restricted := meta.(*AWSClient).IsGovCloud() || meta.(*AWSClient).IsChinaCloud()
   226  
   227  	bucket := d.Get("bucket").(string)
   228  	key := d.Get("key").(string)
   229  
   230  	resp, err := s3conn.HeadObject(
   231  		&s3.HeadObjectInput{
   232  			Bucket: aws.String(bucket),
   233  			Key:    aws.String(key),
   234  		})
   235  
   236  	if err != nil {
   237  		// If S3 returns a 404 Request Failure, mark the object as destroyed
   238  		if awsErr, ok := err.(awserr.RequestFailure); ok && awsErr.StatusCode() == 404 {
   239  			d.SetId("")
   240  			log.Printf("[WARN] Error Reading Object (%s), object not found (HTTP status 404)", key)
   241  			return nil
   242  		}
   243  		return err
   244  	}
   245  	log.Printf("[DEBUG] Reading S3 Bucket Object meta: %s", resp)
   246  
   247  	d.Set("cache_control", resp.CacheControl)
   248  	d.Set("content_disposition", resp.ContentDisposition)
   249  	d.Set("content_encoding", resp.ContentEncoding)
   250  	d.Set("content_language", resp.ContentLanguage)
   251  	d.Set("content_type", resp.ContentType)
   252  	d.Set("version_id", resp.VersionId)
   253  	d.Set("server_side_encryption", resp.ServerSideEncryption)
   254  
   255  	// Only set non-default KMS key ID (one that doesn't match default)
   256  	if resp.SSEKMSKeyId != nil {
   257  		// retrieve S3 KMS Default Master Key
   258  		kmsconn := meta.(*AWSClient).kmsconn
   259  		kmsresp, err := kmsconn.DescribeKey(&kms.DescribeKeyInput{
   260  			KeyId: aws.String("alias/aws/s3"),
   261  		})
   262  		if err != nil {
   263  			return fmt.Errorf("Failed to describe default S3 KMS key (alias/aws/s3): %s", err)
   264  		}
   265  
   266  		if *resp.SSEKMSKeyId != *kmsresp.KeyMetadata.Arn {
   267  			log.Printf("[DEBUG] S3 object is encrypted using a non-default KMS Key ID: %s", *resp.SSEKMSKeyId)
   268  			d.Set("kms_key_id", resp.SSEKMSKeyId)
   269  		}
   270  	}
   271  	d.Set("etag", strings.Trim(*resp.ETag, `"`))
   272  
   273  	// The "STANDARD" (which is also the default) storage
   274  	// class when set would not be included in the results.
   275  	d.Set("storage_class", s3.StorageClassStandard)
   276  	if resp.StorageClass != nil {
   277  		d.Set("storage_class", resp.StorageClass)
   278  	}
   279  
   280  	if !restricted {
   281  		tagResp, err := s3conn.GetObjectTagging(
   282  			&s3.GetObjectTaggingInput{
   283  				Bucket: aws.String(bucket),
   284  				Key:    aws.String(key),
   285  			})
   286  		if err != nil {
   287  			return fmt.Errorf("Failed to get object tags (bucket: %s, key: %s): %s", bucket, key, err)
   288  		}
   289  		d.Set("tags", tagsToMapS3(tagResp.TagSet))
   290  	}
   291  
   292  	return nil
   293  }
   294  
   295  func resourceAwsS3BucketObjectDelete(d *schema.ResourceData, meta interface{}) error {
   296  	s3conn := meta.(*AWSClient).s3conn
   297  
   298  	bucket := d.Get("bucket").(string)
   299  	key := d.Get("key").(string)
   300  
   301  	if _, ok := d.GetOk("version_id"); ok {
   302  		// Bucket is versioned, we need to delete all versions
   303  		vInput := s3.ListObjectVersionsInput{
   304  			Bucket: aws.String(bucket),
   305  			Prefix: aws.String(key),
   306  		}
   307  		out, err := s3conn.ListObjectVersions(&vInput)
   308  		if err != nil {
   309  			return fmt.Errorf("Failed listing S3 object versions: %s", err)
   310  		}
   311  
   312  		for _, v := range out.Versions {
   313  			input := s3.DeleteObjectInput{
   314  				Bucket:    aws.String(bucket),
   315  				Key:       aws.String(key),
   316  				VersionId: v.VersionId,
   317  			}
   318  			_, err := s3conn.DeleteObject(&input)
   319  			if err != nil {
   320  				return fmt.Errorf("Error deleting S3 object version of %s:\n %s:\n %s",
   321  					key, v, err)
   322  			}
   323  		}
   324  	} else {
   325  		// Just delete the object
   326  		input := s3.DeleteObjectInput{
   327  			Bucket: aws.String(bucket),
   328  			Key:    aws.String(key),
   329  		}
   330  		_, err := s3conn.DeleteObject(&input)
   331  		if err != nil {
   332  			return fmt.Errorf("Error deleting S3 bucket object: %s  Bucket: %q Object: %q", err, bucket, key)
   333  		}
   334  	}
   335  
   336  	return nil
   337  }
   338  
   339  func validateS3BucketObjectAclType(v interface{}, k string) (ws []string, errors []error) {
   340  	value := v.(string)
   341  
   342  	cannedAcls := map[string]bool{
   343  		s3.ObjectCannedACLPrivate:                true,
   344  		s3.ObjectCannedACLPublicRead:             true,
   345  		s3.ObjectCannedACLPublicReadWrite:        true,
   346  		s3.ObjectCannedACLAuthenticatedRead:      true,
   347  		s3.ObjectCannedACLAwsExecRead:            true,
   348  		s3.ObjectCannedACLBucketOwnerRead:        true,
   349  		s3.ObjectCannedACLBucketOwnerFullControl: true,
   350  	}
   351  
   352  	sentenceJoin := func(m map[string]bool) string {
   353  		keys := make([]string, 0, len(m))
   354  		for k := range m {
   355  			keys = append(keys, fmt.Sprintf("%q", k))
   356  		}
   357  		sort.Strings(keys)
   358  
   359  		length := len(keys)
   360  		words := make([]string, length)
   361  		copy(words, keys)
   362  
   363  		words[length-1] = fmt.Sprintf("or %s", words[length-1])
   364  		return strings.Join(words, ", ")
   365  	}
   366  
   367  	if _, ok := cannedAcls[value]; !ok {
   368  		errors = append(errors, fmt.Errorf(
   369  			"%q contains an invalid canned ACL type %q. Valid types are either %s",
   370  			k, value, sentenceJoin(cannedAcls)))
   371  	}
   372  	return
   373  }
   374  
   375  func validateS3BucketObjectStorageClassType(v interface{}, k string) (ws []string, errors []error) {
   376  	value := v.(string)
   377  
   378  	storageClass := map[string]bool{
   379  		s3.StorageClassStandard:          true,
   380  		s3.StorageClassReducedRedundancy: true,
   381  		s3.StorageClassStandardIa:        true,
   382  	}
   383  
   384  	if _, ok := storageClass[value]; !ok {
   385  		errors = append(errors, fmt.Errorf(
   386  			"%q contains an invalid Storage Class type %q. Valid types are either %q, %q, or %q",
   387  			k, value, s3.StorageClassStandard, s3.StorageClassReducedRedundancy,
   388  			s3.StorageClassStandardIa))
   389  	}
   390  	return
   391  }
   392  
   393  func validateS3BucketObjectServerSideEncryption(v interface{}, k string) (ws []string, errors []error) {
   394  	value := v.(string)
   395  
   396  	serverSideEncryption := map[string]bool{
   397  		s3.ServerSideEncryptionAes256: true,
   398  		s3.ServerSideEncryptionAwsKms: true,
   399  	}
   400  
   401  	if _, ok := serverSideEncryption[value]; !ok {
   402  		errors = append(errors, fmt.Errorf(
   403  			"%q contains an invalid Server Side Encryption value %q. Valid values are %q and %q",
   404  			k, value, s3.ServerSideEncryptionAes256, s3.ServerSideEncryptionAwsKms))
   405  	}
   406  	return
   407  }