Skip to content

Commit

Permalink
Merge pull request #12945 from hashicorp/f-k8s-config-map-patch
Browse files Browse the repository at this point in the history
kubernetes: Ignore internal K8S annotations in config_map + use PATCH
  • Loading branch information
radeksimko authored Mar 27, 2017
2 parents a516390 + ac87865 commit d622bb4
Show file tree
Hide file tree
Showing 5 changed files with 343 additions and 10 deletions.
135 changes: 135 additions & 0 deletions builtin/providers/kubernetes/patch_operations.go
Original file line number Diff line number Diff line change
@@ -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)
}
126 changes: 126 additions & 0 deletions builtin/providers/kubernetes/patch_operations_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}

}
23 changes: 14 additions & 9 deletions builtin/providers/kubernetes/resource_kubernetes_config_map.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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))
Expand Down
37 changes: 36 additions & 1 deletion builtin/providers/kubernetes/structures.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package kubernetes

import (
"fmt"
"net/url"
"strings"

"github.com/hashicorp/terraform/helper/schema"
api "k8s.io/kubernetes/pkg/api/v1"
)

Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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
}
Loading

0 comments on commit d622bb4

Please sign in to comment.