github.com/vtorhonen/terraform@v0.9.0-beta2.0.20170307220345-5d894e4ffda7/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  	var body io.ReadSeeker
   131  
   132  	if v, ok := d.GetOk("source"); ok {
   133  		source := v.(string)
   134  		path, err := homedir.Expand(source)
   135  		if err != nil {
   136  			return fmt.Errorf("Error expanding homedir in source (%s): %s", source, err)
   137  		}
   138  		file, err := os.Open(path)
   139  		if err != nil {
   140  			return fmt.Errorf("Error opening S3 bucket object source (%s): %s", source, err)
   141  		}
   142  
   143  		body = file
   144  	} else if v, ok := d.GetOk("content"); ok {
   145  		content := v.(string)
   146  		body = bytes.NewReader([]byte(content))
   147  	} else {
   148  		return fmt.Errorf("Must specify \"source\" or \"content\" field")
   149  	}
   150  
   151  	bucket := d.Get("bucket").(string)
   152  	key := d.Get("key").(string)
   153  
   154  	putInput := &s3.PutObjectInput{
   155  		Bucket: aws.String(bucket),
   156  		Key:    aws.String(key),
   157  		ACL:    aws.String(d.Get("acl").(string)),
   158  		Body:   body,
   159  	}
   160  
   161  	if v, ok := d.GetOk("storage_class"); ok {
   162  		putInput.StorageClass = aws.String(v.(string))
   163  	}
   164  
   165  	if v, ok := d.GetOk("cache_control"); ok {
   166  		putInput.CacheControl = aws.String(v.(string))
   167  	}
   168  
   169  	if v, ok := d.GetOk("content_type"); ok {
   170  		putInput.ContentType = aws.String(v.(string))
   171  	}
   172  
   173  	if v, ok := d.GetOk("content_encoding"); ok {
   174  		putInput.ContentEncoding = aws.String(v.(string))
   175  	}
   176  
   177  	if v, ok := d.GetOk("content_language"); ok {
   178  		putInput.ContentLanguage = aws.String(v.(string))
   179  	}
   180  
   181  	if v, ok := d.GetOk("content_disposition"); ok {
   182  		putInput.ContentDisposition = aws.String(v.(string))
   183  	}
   184  
   185  	if v, ok := d.GetOk("server_side_encryption"); ok {
   186  		putInput.ServerSideEncryption = aws.String(v.(string))
   187  	}
   188  
   189  	if v, ok := d.GetOk("kms_key_id"); ok {
   190  		putInput.SSEKMSKeyId = aws.String(v.(string))
   191  		putInput.ServerSideEncryption = aws.String(s3.ServerSideEncryptionAwsKms)
   192  	}
   193  
   194  	if v, ok := d.GetOk("tags"); ok {
   195  		// The tag-set must be encoded as URL Query parameters.
   196  		values := url.Values{}
   197  		for k, v := range v.(map[string]interface{}) {
   198  			values.Add(k, v.(string))
   199  		}
   200  		putInput.Tagging = aws.String(values.Encode())
   201  	}
   202  
   203  	resp, err := s3conn.PutObject(putInput)
   204  	if err != nil {
   205  		return fmt.Errorf("Error putting object in S3 bucket (%s): %s", bucket, err)
   206  	}
   207  
   208  	// See https://forums.aws.amazon.com/thread.jspa?threadID=44003
   209  	d.Set("etag", strings.Trim(*resp.ETag, `"`))
   210  
   211  	d.Set("version_id", resp.VersionId)
   212  	d.SetId(key)
   213  	return resourceAwsS3BucketObjectRead(d, meta)
   214  }
   215  
   216  func resourceAwsS3BucketObjectRead(d *schema.ResourceData, meta interface{}) error {
   217  	s3conn := meta.(*AWSClient).s3conn
   218  
   219  	bucket := d.Get("bucket").(string)
   220  	key := d.Get("key").(string)
   221  
   222  	resp, err := s3conn.HeadObject(
   223  		&s3.HeadObjectInput{
   224  			Bucket: aws.String(bucket),
   225  			Key:    aws.String(key),
   226  		})
   227  
   228  	if err != nil {
   229  		// If S3 returns a 404 Request Failure, mark the object as destroyed
   230  		if awsErr, ok := err.(awserr.RequestFailure); ok && awsErr.StatusCode() == 404 {
   231  			d.SetId("")
   232  			log.Printf("[WARN] Error Reading Object (%s), object not found (HTTP status 404)", key)
   233  			return nil
   234  		}
   235  		return err
   236  	}
   237  	log.Printf("[DEBUG] Reading S3 Bucket Object meta: %s", resp)
   238  
   239  	d.Set("cache_control", resp.CacheControl)
   240  	d.Set("content_disposition", resp.ContentDisposition)
   241  	d.Set("content_encoding", resp.ContentEncoding)
   242  	d.Set("content_language", resp.ContentLanguage)
   243  	d.Set("content_type", resp.ContentType)
   244  	d.Set("version_id", resp.VersionId)
   245  	d.Set("server_side_encryption", resp.ServerSideEncryption)
   246  
   247  	// Only set non-default KMS key ID (one that doesn't match default)
   248  	if resp.SSEKMSKeyId != nil {
   249  		// retrieve S3 KMS Default Master Key
   250  		kmsconn := meta.(*AWSClient).kmsconn
   251  		kmsresp, err := kmsconn.DescribeKey(&kms.DescribeKeyInput{
   252  			KeyId: aws.String("alias/aws/s3"),
   253  		})
   254  		if err != nil {
   255  			return fmt.Errorf("Failed to describe default S3 KMS key (alias/aws/s3): %s", err)
   256  		}
   257  
   258  		if *resp.SSEKMSKeyId != *kmsresp.KeyMetadata.Arn {
   259  			log.Printf("[DEBUG] S3 object is encrypted using a non-default KMS Key ID: %s", *resp.SSEKMSKeyId)
   260  			d.Set("kms_key_id", resp.SSEKMSKeyId)
   261  		}
   262  	}
   263  	d.Set("etag", strings.Trim(*resp.ETag, `"`))
   264  
   265  	// The "STANDARD" (which is also the default) storage
   266  	// class when set would not be included in the results.
   267  	d.Set("storage_class", s3.StorageClassStandard)
   268  	if resp.StorageClass != nil {
   269  		d.Set("storage_class", resp.StorageClass)
   270  	}
   271  
   272  	tagResp, err := s3conn.GetObjectTagging(
   273  		&s3.GetObjectTaggingInput{
   274  			Bucket: aws.String(bucket),
   275  			Key:    aws.String(key),
   276  		})
   277  	if err != nil {
   278  		return err
   279  	}
   280  	d.Set("tags", tagsToMapS3(tagResp.TagSet))
   281  
   282  	return nil
   283  }
   284  
   285  func resourceAwsS3BucketObjectDelete(d *schema.ResourceData, meta interface{}) error {
   286  	s3conn := meta.(*AWSClient).s3conn
   287  
   288  	bucket := d.Get("bucket").(string)
   289  	key := d.Get("key").(string)
   290  
   291  	if _, ok := d.GetOk("version_id"); ok {
   292  		// Bucket is versioned, we need to delete all versions
   293  		vInput := s3.ListObjectVersionsInput{
   294  			Bucket: aws.String(bucket),
   295  			Prefix: aws.String(key),
   296  		}
   297  		out, err := s3conn.ListObjectVersions(&vInput)
   298  		if err != nil {
   299  			return fmt.Errorf("Failed listing S3 object versions: %s", err)
   300  		}
   301  
   302  		for _, v := range out.Versions {
   303  			input := s3.DeleteObjectInput{
   304  				Bucket:    aws.String(bucket),
   305  				Key:       aws.String(key),
   306  				VersionId: v.VersionId,
   307  			}
   308  			_, err := s3conn.DeleteObject(&input)
   309  			if err != nil {
   310  				return fmt.Errorf("Error deleting S3 object version of %s:\n %s:\n %s",
   311  					key, v, err)
   312  			}
   313  		}
   314  	} else {
   315  		// Just delete the object
   316  		input := s3.DeleteObjectInput{
   317  			Bucket: aws.String(bucket),
   318  			Key:    aws.String(key),
   319  		}
   320  		_, err := s3conn.DeleteObject(&input)
   321  		if err != nil {
   322  			return fmt.Errorf("Error deleting S3 bucket object: %s", err)
   323  		}
   324  	}
   325  
   326  	return nil
   327  }
   328  
   329  func validateS3BucketObjectAclType(v interface{}, k string) (ws []string, errors []error) {
   330  	value := v.(string)
   331  
   332  	cannedAcls := map[string]bool{
   333  		s3.ObjectCannedACLPrivate:                true,
   334  		s3.ObjectCannedACLPublicRead:             true,
   335  		s3.ObjectCannedACLPublicReadWrite:        true,
   336  		s3.ObjectCannedACLAuthenticatedRead:      true,
   337  		s3.ObjectCannedACLAwsExecRead:            true,
   338  		s3.ObjectCannedACLBucketOwnerRead:        true,
   339  		s3.ObjectCannedACLBucketOwnerFullControl: true,
   340  	}
   341  
   342  	sentenceJoin := func(m map[string]bool) string {
   343  		keys := make([]string, 0, len(m))
   344  		for k := range m {
   345  			keys = append(keys, fmt.Sprintf("%q", k))
   346  		}
   347  		sort.Strings(keys)
   348  
   349  		length := len(keys)
   350  		words := make([]string, length)
   351  		copy(words, keys)
   352  
   353  		words[length-1] = fmt.Sprintf("or %s", words[length-1])
   354  		return strings.Join(words, ", ")
   355  	}
   356  
   357  	if _, ok := cannedAcls[value]; !ok {
   358  		errors = append(errors, fmt.Errorf(
   359  			"%q contains an invalid canned ACL type %q. Valid types are either %s",
   360  			k, value, sentenceJoin(cannedAcls)))
   361  	}
   362  	return
   363  }
   364  
   365  func validateS3BucketObjectStorageClassType(v interface{}, k string) (ws []string, errors []error) {
   366  	value := v.(string)
   367  
   368  	storageClass := map[string]bool{
   369  		s3.StorageClassStandard:          true,
   370  		s3.StorageClassReducedRedundancy: true,
   371  		s3.StorageClassStandardIa:        true,
   372  	}
   373  
   374  	if _, ok := storageClass[value]; !ok {
   375  		errors = append(errors, fmt.Errorf(
   376  			"%q contains an invalid Storage Class type %q. Valid types are either %q, %q, or %q",
   377  			k, value, s3.StorageClassStandard, s3.StorageClassReducedRedundancy,
   378  			s3.StorageClassStandardIa))
   379  	}
   380  	return
   381  }
   382  
   383  func validateS3BucketObjectServerSideEncryption(v interface{}, k string) (ws []string, errors []error) {
   384  	value := v.(string)
   385  
   386  	serverSideEncryption := map[string]bool{
   387  		s3.ServerSideEncryptionAes256: true,
   388  		s3.ServerSideEncryptionAwsKms: true,
   389  	}
   390  
   391  	if _, ok := serverSideEncryption[value]; !ok {
   392  		errors = append(errors, fmt.Errorf(
   393  			"%q contains an invalid Server Side Encryption value %q. Valid values are %q and %q",
   394  			k, value, s3.ServerSideEncryptionAes256, s3.ServerSideEncryptionAwsKms))
   395  	}
   396  	return
   397  }