diff --git a/builtin/providers/kubernetes/patch_operations.go b/builtin/providers/kubernetes/patch_operations.go new file mode 100644 index 000000000000..74965b137357 --- /dev/null +++ b/builtin/providers/kubernetes/patch_operations.go @@ -0,0 +1,135 @@ +package kubernetes + +import ( + "encoding/json" + "reflect" + "sort" + "strings" +) + +func diffStringMap(pathPrefix string, oldV, newV map[string]interface{}) PatchOperations { + ops := make([]PatchOperation, 0, 0) + + pathPrefix = strings.TrimRight(pathPrefix, "/") + + // This is suboptimal for adding whole new map from scratch + // or deleting the whole map, but it's actually intention. + // There may be some other map items managed outside of TF + // and we don't want to touch these. + + for k, _ := range oldV { + if _, ok := newV[k]; ok { + continue + } + ops = append(ops, &RemoveOperation{Path: pathPrefix + "/" + k}) + } + + for k, v := range newV { + newValue := v.(string) + + if oldValue, ok := oldV[k].(string); ok { + if oldValue == newValue { + continue + } + + ops = append(ops, &ReplaceOperation{ + Path: pathPrefix + "/" + k, + Value: newValue, + }) + continue + } + + ops = append(ops, &AddOperation{ + Path: pathPrefix + "/" + k, + Value: newValue, + }) + } + + return ops +} + +type PatchOperations []PatchOperation + +func (po PatchOperations) MarshalJSON() ([]byte, error) { + var v []PatchOperation = po + return json.Marshal(v) +} + +func (po PatchOperations) Equal(ops []PatchOperation) bool { + var v []PatchOperation = po + + sort.Slice(v, sortByPathAsc(ops)) + sort.Slice(ops, sortByPathAsc(ops)) + + return reflect.DeepEqual(v, ops) +} + +func sortByPathAsc(ops []PatchOperation) func(i, j int) bool { + return func(i, j int) bool { + return ops[i].GetPath() < ops[j].GetPath() + } +} + +type PatchOperation interface { + MarshalJSON() ([]byte, error) + GetPath() string +} + +type ReplaceOperation struct { + Path string `json:"path"` + Value interface{} `json:"value"` + Op string `json:"op"` +} + +func (o *ReplaceOperation) GetPath() string { + return o.Path +} + +func (o *ReplaceOperation) MarshalJSON() ([]byte, error) { + o.Op = "replace" + return json.Marshal(*o) +} + +func (o *ReplaceOperation) String() string { + b, _ := o.MarshalJSON() + return string(b) +} + +type AddOperation struct { + Path string `json:"path"` + Value interface{} `json:"value"` + Op string `json:"op"` +} + +func (o *AddOperation) GetPath() string { + return o.Path +} + +func (o *AddOperation) MarshalJSON() ([]byte, error) { + o.Op = "add" + return json.Marshal(*o) +} + +func (o *AddOperation) String() string { + b, _ := o.MarshalJSON() + return string(b) +} + +type RemoveOperation struct { + Path string `json:"path"` + Op string `json:"op"` +} + +func (o *RemoveOperation) GetPath() string { + return o.Path +} + +func (o *RemoveOperation) MarshalJSON() ([]byte, error) { + o.Op = "remove" + return json.Marshal(*o) +} + +func (o *RemoveOperation) String() string { + b, _ := o.MarshalJSON() + return string(b) +} diff --git a/builtin/providers/kubernetes/patch_operations_test.go b/builtin/providers/kubernetes/patch_operations_test.go new file mode 100644 index 000000000000..c60a5e628c89 --- /dev/null +++ b/builtin/providers/kubernetes/patch_operations_test.go @@ -0,0 +1,126 @@ +package kubernetes + +import ( + "fmt" + "testing" +) + +func TestDiffStringMap(t *testing.T) { + testCases := []struct { + Path string + Old map[string]interface{} + New map[string]interface{} + ExpectedOps PatchOperations + }{ + { + Path: "/parent/", + Old: map[string]interface{}{ + "one": "111", + "two": "222", + }, + New: map[string]interface{}{ + "one": "111", + "two": "222", + "three": "333", + }, + ExpectedOps: []PatchOperation{ + &AddOperation{ + Path: "/parent/three", + Value: "333", + }, + }, + }, + { + Path: "/parent/", + Old: map[string]interface{}{ + "one": "111", + "two": "222", + }, + New: map[string]interface{}{ + "one": "111", + "two": "abcd", + }, + ExpectedOps: []PatchOperation{ + &ReplaceOperation{ + Path: "/parent/two", + Value: "abcd", + }, + }, + }, + { + Path: "/parent/", + Old: map[string]interface{}{ + "one": "111", + "two": "222", + }, + New: map[string]interface{}{ + "two": "abcd", + "three": "333", + }, + ExpectedOps: []PatchOperation{ + &RemoveOperation{Path: "/parent/one"}, + &ReplaceOperation{ + Path: "/parent/two", + Value: "abcd", + }, + &AddOperation{ + Path: "/parent/three", + Value: "333", + }, + }, + }, + { + Path: "/parent/", + Old: map[string]interface{}{ + "one": "111", + "two": "222", + }, + New: map[string]interface{}{ + "two": "222", + }, + ExpectedOps: []PatchOperation{ + &RemoveOperation{Path: "/parent/one"}, + }, + }, + { + Path: "/parent/", + Old: map[string]interface{}{ + "one": "111", + "two": "222", + }, + New: map[string]interface{}{}, + ExpectedOps: []PatchOperation{ + &RemoveOperation{Path: "/parent/one"}, + &RemoveOperation{Path: "/parent/two"}, + }, + }, + { + Path: "/parent/", + Old: map[string]interface{}{}, + New: map[string]interface{}{ + "one": "111", + "two": "222", + }, + ExpectedOps: []PatchOperation{ + &AddOperation{ + Path: "/parent/one", + Value: "111", + }, + &AddOperation{ + Path: "/parent/two", + Value: "222", + }, + }, + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + ops := diffStringMap(tc.Path, tc.Old, tc.New) + if !tc.ExpectedOps.Equal(ops) { + t.Fatalf("Operations don't match.\nExpected: %v\nGiven: %v\n", tc.ExpectedOps, ops) + } + }) + } + +} diff --git a/builtin/providers/kubernetes/resource_kubernetes_config_map.go b/builtin/providers/kubernetes/resource_kubernetes_config_map.go index 460ca638e79c..d24fa1436076 100644 --- a/builtin/providers/kubernetes/resource_kubernetes_config_map.go +++ b/builtin/providers/kubernetes/resource_kubernetes_config_map.go @@ -1,9 +1,11 @@ package kubernetes import ( + "fmt" "log" "github.com/hashicorp/terraform/helper/schema" + pkgApi "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/errors" api "k8s.io/kubernetes/pkg/api/v1" kubernetes "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5" @@ -73,19 +75,22 @@ func resourceKubernetesConfigMapRead(d *schema.ResourceData, meta interface{}) e func resourceKubernetesConfigMapUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*kubernetes.Clientset) - metadata := expandMetadata(d.Get("metadata").([]interface{})) namespace, name := idParts(d.Id()) - // This is necessary in case the name is generated - metadata.Name = name - cfgMap := api.ConfigMap{ - ObjectMeta: metadata, - Data: expandStringMap(d.Get("data").(map[string]interface{})), + ops := patchMetadata("metadata.0.", "/metadata/", d) + if d.HasChange("data") { + oldV, newV := d.GetChange("data") + diffOps := diffStringMap("/data/", oldV.(map[string]interface{}), newV.(map[string]interface{})) + ops = append(ops, diffOps...) } - log.Printf("[INFO] Updating config map: %#v", cfgMap) - out, err := conn.CoreV1().ConfigMaps(namespace).Update(&cfgMap) + data, err := ops.MarshalJSON() if err != nil { - return err + return fmt.Errorf("Failed to marshal update operations: %s", err) + } + log.Printf("[INFO] Updating config map %q: %v", name, string(data)) + out, err := conn.CoreV1().ConfigMaps(namespace).Patch(name, pkgApi.JSONPatchType, data) + if err != nil { + return fmt.Errorf("Failed to update Config Map: %s", err) } log.Printf("[INFO] Submitted updated config map: %#v", out) d.SetId(buildId(out.ObjectMeta)) diff --git a/builtin/providers/kubernetes/structures.go b/builtin/providers/kubernetes/structures.go index 8b98cee3277c..58bc49030c53 100644 --- a/builtin/providers/kubernetes/structures.go +++ b/builtin/providers/kubernetes/structures.go @@ -2,8 +2,10 @@ package kubernetes import ( "fmt" + "net/url" "strings" + "github.com/hashicorp/terraform/helper/schema" api "k8s.io/kubernetes/pkg/api/v1" ) @@ -39,6 +41,21 @@ func expandMetadata(in []interface{}) api.ObjectMeta { return meta } +func patchMetadata(keyPrefix, pathPrefix string, d *schema.ResourceData) PatchOperations { + ops := make([]PatchOperation, 0, 0) + if d.HasChange(keyPrefix + "annotations") { + oldV, newV := d.GetChange(keyPrefix + "annotations") + diffOps := diffStringMap(pathPrefix+"annotations", oldV.(map[string]interface{}), newV.(map[string]interface{})) + ops = append(ops, diffOps...) + } + if d.HasChange(keyPrefix + "labels") { + oldV, newV := d.GetChange(keyPrefix + "labels") + diffOps := diffStringMap(pathPrefix+"labels", oldV.(map[string]interface{}), newV.(map[string]interface{})) + ops = append(ops, diffOps...) + } + return ops +} + func expandStringMap(m map[string]interface{}) map[string]string { result := make(map[string]string) for k, v := range m { @@ -49,7 +66,7 @@ func expandStringMap(m map[string]interface{}) map[string]string { func flattenMetadata(meta api.ObjectMeta) []map[string]interface{} { m := make(map[string]interface{}) - m["annotations"] = meta.Annotations + m["annotations"] = filterAnnotations(meta.Annotations) m["generate_name"] = meta.GenerateName m["labels"] = meta.Labels m["name"] = meta.Name @@ -64,3 +81,21 @@ func flattenMetadata(meta api.ObjectMeta) []map[string]interface{} { return []map[string]interface{}{m} } + +func filterAnnotations(m map[string]string) map[string]string { + for k, _ := range m { + if isInternalAnnotationKey(k) { + delete(m, k) + } + } + return m +} + +func isInternalAnnotationKey(annotationKey string) bool { + u, err := url.Parse("//" + annotationKey) + if err == nil && strings.HasSuffix(u.Hostname(), "kubernetes.io") { + return true + } + + return false +} diff --git a/builtin/providers/kubernetes/structures_test.go b/builtin/providers/kubernetes/structures_test.go new file mode 100644 index 000000000000..2a0b9003e8bc --- /dev/null +++ b/builtin/providers/kubernetes/structures_test.go @@ -0,0 +1,32 @@ +package kubernetes + +import ( + "fmt" + "testing" +) + +func TestIsInternalAnnotationKey(t *testing.T) { + testCases := []struct { + Key string + Expected bool + }{ + {"", false}, + {"anyKey", false}, + {"any.hostname.io", false}, + {"any.hostname.com/with/path", false}, + {"any.kubernetes.io", true}, + {"kubernetes.io", true}, + {"pv.kubernetes.io/any/path", true}, + } + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + isInternal := isInternalAnnotationKey(tc.Key) + if tc.Expected && isInternal != tc.Expected { + t.Fatalf("Expected %q to be internal", tc.Key) + } + if !tc.Expected && isInternal != tc.Expected { + t.Fatalf("Expected %q not to be internal", tc.Key) + } + }) + } +}