kubernetes: Use JSON patch for updating config_map

This commit is contained in:
Radek Simko 2017-03-21 21:01:18 +00:00
parent 98ee99f405
commit 4d2e28aecb
No known key found for this signature in database
GPG Key ID: 6823F3DCCE01BB19
4 changed files with 291 additions and 9 deletions

View File

@ -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)
}

View File

@ -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)
}
})
}
}

View File

@ -1,9 +1,11 @@
package kubernetes package kubernetes
import ( import (
"fmt"
"log" "log"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
pkgApi "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/errors" "k8s.io/kubernetes/pkg/api/errors"
api "k8s.io/kubernetes/pkg/api/v1" api "k8s.io/kubernetes/pkg/api/v1"
kubernetes "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5" 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 { func resourceKubernetesConfigMapUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*kubernetes.Clientset) conn := meta.(*kubernetes.Clientset)
metadata := expandMetadata(d.Get("metadata").([]interface{}))
namespace, name := idParts(d.Id()) namespace, name := idParts(d.Id())
// This is necessary in case the name is generated
metadata.Name = name
cfgMap := api.ConfigMap{ ops := patchMetadata("metadata.0.", "/metadata/", d)
ObjectMeta: metadata, if d.HasChange("data") {
Data: expandStringMap(d.Get("data").(map[string]interface{})), 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) data, err := ops.MarshalJSON()
out, err := conn.CoreV1().ConfigMaps(namespace).Update(&cfgMap)
if err != nil { 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) log.Printf("[INFO] Submitted updated config map: %#v", out)
d.SetId(buildId(out.ObjectMeta)) d.SetId(buildId(out.ObjectMeta))

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/hashicorp/terraform/helper/schema"
api "k8s.io/kubernetes/pkg/api/v1" api "k8s.io/kubernetes/pkg/api/v1"
) )
@ -39,6 +40,21 @@ func expandMetadata(in []interface{}) api.ObjectMeta {
return meta 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 { func expandStringMap(m map[string]interface{}) map[string]string {
result := make(map[string]string) result := make(map[string]string)
for k, v := range m { for k, v := range m {