diff --git a/builtin/providers/aws/resource_aws_s3_bucket.go b/builtin/providers/aws/resource_aws_s3_bucket.go index ec57452022c9..3c4d9fecadff 100644 --- a/builtin/providers/aws/resource_aws_s3_bucket.go +++ b/builtin/providers/aws/resource_aws_s3_bucket.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "log" + "sort" "github.com/hashicorp/terraform/helper/schema" @@ -718,6 +719,8 @@ func normalizeJson(jsonString interface{}) string { if err != nil { return fmt.Sprintf("Error parsing JSON: %s", err) } + normalizePolicyStatements(j) + sortJSONArray(j) b, _ := json.Marshal(j) return string(b[:]) } @@ -735,3 +738,61 @@ func normalizeRegion(region string) string { type S3Website struct { Endpoint, Domain string } + +// S3 bucket policy parsing logic - this is necessary as AWS sorts some +// policy sections (ie: Action lists in policy statements), but Unmarshal/ +// Marshal does not. + +// multiArray allows us to define a slice of interfaces to sort, versus +// a slice of strings. +type multiArray []interface{} + +func (a multiArray) Len() int { return len(a) } +func (a multiArray) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +// multiArray.Less() is implemented to sort only on neighboring string values, +// so if the array contains non-string values, this will give unstable results. +func (a multiArray) Less(i, j int) bool { + switch ival := a[i].(type) { + case string: + if jval, ok := a[j].(string); ok { + log.Printf("[DEBUG] sort: %s < %s", ival, jval) + return ival < jval + } + } + return false +} + +// sortJsonArray recursively looks for arrays and sorts them. +func sortJSONArray(d interface{}) { + switch e := d.(type) { + case map[string]interface{}: + for k, v := range e { + log.Printf("[DEBUG] recursing into %s", k) + sortJSONArray(v) + } + case []interface{}: + log.Printf("Found array, length %d", len(e)) + sort.Sort(multiArray(e)) + for _, v := range e { + log.Printf("[DEBUG] recursing into %s", v) + sortJSONArray(v) + } + } +} + +// normalizePolicyStatements performs the following fixups to a policy: +// * Populate policy statement's Sid as empty if it is not present +// * Set action's value to a single string if only one element is present +func normalizePolicyStatements(policy map[string]interface{}) { + for _, v := range policy["Statement"].([]interface{}) { + if _, ok := v.(map[string]interface{})["Sid"]; ok == false { + v.(map[string]interface{})["Sid"] = "" + } + if actions, ok := v.(map[string]interface{})["Action"].([]interface{}); ok { + if len(actions) < 2 { + v.(map[string]interface{})["Action"] = actions[0].(string) + } + } + } +}