Merge pull request #12945 from hashicorp/f-k8s-config-map-patch

kubernetes: Ignore internal K8S annotations in config_map + use PATCH
This commit is contained in:
Radek Simko 2017-03-27 16:39:16 +01:00 committed by GitHub
commit d622bb46c4
5 changed files with 343 additions and 10 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
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))

View File

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

View File

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