From b61553e584a1760f31703a0cb811bf3723c43c1d Mon Sep 17 00:00:00 2001 From: Donny Xia Date: Tue, 28 Jul 2020 15:03:05 -0700 Subject: [PATCH] refactor hasher --- api/hasher/hasher.go | 140 ++++++++++++ api/hasher/hasher_test.go | 316 ++++++++++++++++++++++++++- api/k8sdeps/kunstruct/hasher.go | 119 +--------- api/k8sdeps/kunstruct/hasher_test.go | 224 ------------------- 4 files changed, 457 insertions(+), 342 deletions(-) delete mode 100644 api/k8sdeps/kunstruct/hasher_test.go diff --git a/api/hasher/hasher.go b/api/hasher/hasher.go index fe54a1025..c106c3176 100644 --- a/api/hasher/hasher.go +++ b/api/hasher/hasher.go @@ -8,6 +8,8 @@ import ( "encoding/json" "fmt" "sort" + + "sigs.k8s.io/kustomize/kyaml/yaml" ) // SortArrayAndComputeHash sorts a string array and @@ -50,3 +52,141 @@ func Encode(hex string) (string, error) { func Hash(data string) string { return fmt.Sprintf("%x", sha256.Sum256([]byte(data))) } + +// HashRNode returns the hash value of input RNode +func HashRNode(node *yaml.RNode) (string, error) { + // get node kind + kindNode, err := node.Pipe(yaml.FieldMatcher{Name: "kind"}) + if err != nil { + return "", err + } + kind := kindNode.YNode().Value + + // calculate hash for different kinds + switch kind { + case "ConfigMap": + return configMapHash(node) + case "Secret": + return secretHash(node) + default: + return defaultHash(node) + } +} + +// configMapHash returns a hash of the ConfigMap. +// The Data, Kind, and Name are taken into account. +func configMapHash(node *yaml.RNode) (string, error) { + encoded, err := encodeConfigMap(node) + if err != nil { + return "", err + } + h, err := Encode(Hash(encoded)) + if err != nil { + return "", err + } + return h, nil +} + +// SecretHash returns a hash of the Secret. +// The Data, Kind, Name, and Type are taken into account. +func secretHash(node *yaml.RNode) (string, error) { + encoded, err := encodeSecret(node) + if err != nil { + return "", err + } + h, err := Encode(Hash(encoded)) + if err != nil { + return "", err + } + return h, nil +} + +// unstructuredHash creates a hash for an arbitrary type. +// All fields of the object are taken into account when generating the hash. +// This is a fallback for when a specalised hash for the type is unavailable. +func defaultHash(node *yaml.RNode) (string, error) { + encoded, err := json.Marshal(node.YNode()) + if err != nil { + return "", err + } + h, err := Encode(Hash(string(encoded))) + if err != nil { + return "", err + } + return h, nil +} + +func getNodeValues(node *yaml.RNode, paths []string) (map[string]interface{}, error) { + values := make(map[string]interface{}) + for _, p := range paths { + vn, err := node.Pipe(yaml.Lookup(p)) + if err != nil { + return map[string]interface{}{}, err + } + if vn == nil { + values[p] = "" + continue + } + if vn.YNode().Kind != yaml.ScalarNode { + vs, err := vn.MarshalJSON() + if err != nil { + return map[string]interface{}{}, err + } + // data, binaryData and stringData are all maps + var v map[string]interface{} + json.Unmarshal(vs, &v) + values[p] = v + } else { + values[p] = vn.YNode().Value + } + } + return values, nil +} + +// encodeConfigMap encodes a ConfigMap. +// Data, Kind, and Name are taken into account. +// BinaryData is included if it's not empty to avoid useless key in output. +func encodeConfigMap(node *yaml.RNode) (string, error) { + // get fields + paths := []string{"metadata/name", "data", "binaryData"} + values, err := getNodeValues(node, paths) + if err != nil { + return "", err + } + m := map[string]interface{}{"kind": "ConfigMap", "name": values["metadata/name"], + "data": values["data"]} + if _, ok := values["binaryData"].(map[string]interface{}); ok { + m["binaryData"] = values["binaryData"] + } + + // json.Marshal sorts the keys in a stable order in the encoding + data, err := json.Marshal(m) + if err != nil { + return "", err + } + return string(data), nil +} + +// encodeSecret encodes a Secret. +// Data, Kind, Name, and Type are taken into account. +// StringData is included if it's not empty to avoid useless key in output. +func encodeSecret(node *yaml.RNode) (string, error) { + // get fields + paths := []string{"type", "metadata/name", "data", "stringData"} + values, err := getNodeValues(node, paths) + if err != nil { + return "", err + } + m := map[string]interface{}{"kind": "Secret", "type": values["type"], + "name": values["metadata/name"], "data": values["data"]} + if _, ok := values["stringData"].(map[string]interface{}); ok { + m["stringData"] = values["stringData"] + } + + // json.Marshal sorts the keys in a stable order in the encoding + data, err := json.Marshal(m) + if err != nil { + return "", err + } + return string(data), nil +} diff --git a/api/hasher/hasher_test.go b/api/hasher/hasher_test.go index 4cd8e2f57..c5f3ad66d 100644 --- a/api/hasher/hasher_test.go +++ b/api/hasher/hasher_test.go @@ -1,12 +1,13 @@ // Copyright 2019 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 -package hasher_test +package hasher import ( + "strings" "testing" - . "sigs.k8s.io/kustomize/api/hasher" + "sigs.k8s.io/kustomize/kyaml/yaml" ) func TestSortArrayAndComputeHash(t *testing.T) { @@ -39,3 +40,314 @@ func TestHash(t *testing.T) { t.Errorf("expected hash %q but got %q", expect, sum) } } + +func TestConfigMapHash(t *testing.T) { + cases := []struct { + desc string + cmYaml string + hash string + err string + }{ + // empty map + {"empty data", ` +apiVersion: v1 +kind: ConfigMap`, "6ct58987ht", ""}, + // one key + {"one key", ` +apiVersion: v1 +kind: ConfigMap +data: + one: ""`, "9g67k2htb6", ""}, + // three keys (tests sorting order) + {"three keys", ` +apiVersion: v1 +kind: ConfigMap +data: + two: 2 + one: "" + three: 3`, "7757f9kkct", ""}, + // empty binary data map + {"empty binary data", ` +apiVersion: v1 +kind: ConfigMap`, "6ct58987ht", ""}, + // one key with binary data + {"one key with binary data", ` +apiVersion: v1 +kind: ConfigMap +binaryData: + one: ""`, "6mtk2m274t", ""}, + // three keys with binary data (tests sorting order) + {"three keys with binary data", ` +apiVersion: v1 +kind: ConfigMap +binaryData: + two: 2 + one: "" + three: 3`, "9th7kc28dg", ""}, + // two keys, one with string and another with binary data + {"two keys with one each", ` +apiVersion: v1 +kind: ConfigMap +data: + one: "" +binaryData: + two: ""`, "698h7c7t9m", ""}, + } + + for _, c := range cases { + node, err := yaml.Parse(c.cmYaml) + if err != nil { + t.Fatal(err) + } + h, err := configMapHash(node) + if SkipRest(t, c.desc, err, c.err) { + continue + } + if c.hash != h { + t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h) + } + } +} + +func TestSecretHash(t *testing.T) { + cases := []struct { + desc string + secretYaml string + hash string + err string + }{ + // empty map + {"empty data", ` +apiVersion: v1 +kind: Secret +type: my-type`, "5gmgkf8578", ""}, + // one key + {"one key", ` +apiVersion: v1 +kind: Secret +type: my-type +data: + one: ""`, "74bd68bm66", ""}, + // three keys (tests sorting order) + {"three keys", ` +apiVersion: v1 +kind: Secret +type: my-type +data: + two: 2 + one: "" + three: 3`, "4gf75c7476", ""}, + // with stringdata + {"stringdata", ` +apiVersion: v1 +kind: Secret +type: my-type +data: + one: "" +stringData: + two: 2`, "c4h4264gdb", ""}, + // empty stringdata + {"empty stringdata", ` +apiVersion: v1 +kind: Secret +type: my-type +data: + one: ""`, "74bd68bm66", ""}, + } + + for _, c := range cases { + node, err := yaml.Parse(c.secretYaml) + if err != nil { + t.Fatal(err) + } + h, err := secretHash(node) + if SkipRest(t, c.desc, err, c.err) { + continue + } + if c.hash != h { + t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h) + } + } +} + +func TestUnstructuredHash(t *testing.T) { + cases := []struct { + desc string + unstructured string + hash string + err string + }{ + {"minimal", ` +apiVersion: test/v1 +kind: TestResource +metadata: + name: my-resource`, "244782mkb7", ""}, + {"with spec", ` +apiVersion: test/v1 +kind: TestResource +metadata: + name: my-resource +spec: + foo: 1 + bar: abc`, "59m2mdccg4", ""}, + } + + for _, c := range cases { + node, err := yaml.Parse(c.unstructured) + if err != nil { + t.Fatal(err) + } + h, err := defaultHash(node) + if SkipRest(t, c.desc, err, c.err) { + continue + } + if c.hash != h { + t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h) + } + } +} + +func TestEncodeConfigMap(t *testing.T) { + cases := []struct { + desc string + cmYaml string + expect string + err string + }{ + // empty map + {"empty data", ` +apiVersion: v1 +kind: ConfigMap`, `{"data":"","kind":"ConfigMap","name":""}`, ""}, + // one key + {"one key", ` +apiVersion: v1 +kind: ConfigMap +data: + one: ""`, `{"data":{"one":""},"kind":"ConfigMap","name":""}`, ""}, + // three keys (tests sorting order) + {"three keys", ` +apiVersion: v1 +kind: ConfigMap +data: + two: 2 + one: "" + three: 3`, `{"data":{"one":"","three":3,"two":2},"kind":"ConfigMap","name":""}`, ""}, + // empty binary map + {"empty data", ` +apiVersion: v1 +kind: ConfigMap`, `{"data":"","kind":"ConfigMap","name":""}`, ""}, + // one key with binary data + {"one key", ` +apiVersion: v1 +kind: ConfigMap +binaryData: + one: ""`, `{"binaryData":{"one":""},"data":"","kind":"ConfigMap","name":""}`, ""}, + // three keys with binary data (tests sorting order) + {"three keys", ` +apiVersion: v1 +kind: ConfigMap +binaryData: + two: 2 + one: "" + three: 3`, `{"binaryData":{"one":"","three":3,"two":2},"data":"","kind":"ConfigMap","name":""}`, ""}, + // two keys, one string and one binary values + {"two keys with one each", ` +apiVersion: v1 +kind: ConfigMap +data: + one: "" +binaryData: + two: ""`, `{"binaryData":{"two":""},"data":{"one":""},"kind":"ConfigMap","name":""}`, ""}, + } + for _, c := range cases { + node, err := yaml.Parse(c.cmYaml) + if err != nil { + t.Fatal(err) + } + s, err := encodeConfigMap(node) + if SkipRest(t, c.desc, err, c.err) { + continue + } + if s != c.expect { + t.Errorf("case %q, expect %q but got %q from encode %#v", c.desc, c.expect, s, c.cmYaml) + } + } +} + +func TestEncodeSecret(t *testing.T) { + cases := []struct { + desc string + secretYaml string + expect string + err string + }{ + // empty map + {"empty data", ` +apiVersion: v1 +kind: Secret +type: my-type`, `{"data":"","kind":"Secret","name":"","type":"my-type"}`, ""}, + // one key + {"one key", ` +apiVersion: v1 +kind: Secret +type: my-type +data: + one: ""`, `{"data":{"one":""},"kind":"Secret","name":"","type":"my-type"}`, ""}, + // three keys (tests sorting order) - note json.Marshal base64 encodes the values because they come in as []byte + {"three keys", ` +apiVersion: v1 +kind: Secret +type: my-type +data: + two: 2 + one: "" + three: 3`, `{"data":{"one":"","three":3,"two":2},"kind":"Secret","name":"","type":"my-type"}`, ""}, + // with stringdata + {"stringdata", ` +apiVersion: v1 +kind: Secret +type: my-type +data: + one: "" +stringData: + two: 2`, `{"data":{"one":""},"kind":"Secret","name":"","stringData":{"two":2},"type":"my-type"}`, ""}, + // empty stringdata + {"empty stringdata", ` +apiVersion: v1 +kind: Secret +type: my-type +data: + one: ""`, `{"data":{"one":""},"kind":"Secret","name":"","type":"my-type"}`, ""}, + } + for _, c := range cases { + node, err := yaml.Parse(c.secretYaml) + if err != nil { + t.Fatal(err) + } + s, err := encodeSecret(node) + if SkipRest(t, c.desc, err, c.err) { + continue + } + if s != c.expect { + t.Errorf("case %q, expect %q but got %q from encode %#v", c.desc, c.expect, s, c.secretYaml) + } + } +} + +// SkipRest returns true if there was a non-nil error or if we expected an error that didn't happen, +// and logs the appropriate error on the test object. +// The return value indicates whether we should skip the rest of the test case due to the error result. +func SkipRest(t *testing.T, desc string, err error, contains string) bool { + if err != nil { + if len(contains) == 0 { + t.Errorf("case %q, expect nil error but got %q", desc, err.Error()) + } else if !strings.Contains(err.Error(), contains) { + t.Errorf("case %q, expect error to contain %q but got %q", desc, contains, err.Error()) + } + return true + } else if len(contains) > 0 { + t.Errorf("case %q, expect error to contain %q but got nil error", desc, contains) + return true + } + return false +} diff --git a/api/k8sdeps/kunstruct/hasher.go b/api/k8sdeps/kunstruct/hasher.go index 1b263fede..41f0ddbb9 100644 --- a/api/k8sdeps/kunstruct/hasher.go +++ b/api/k8sdeps/kunstruct/hasher.go @@ -4,12 +4,9 @@ package kunstruct import ( - "encoding/json" - - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/kustomize/api/hasher" "sigs.k8s.io/kustomize/api/ifc" + "sigs.k8s.io/kustomize/kyaml/filtersutil" ) // kustHash computes a hash of an unstructured object. @@ -22,119 +19,9 @@ func NewKustHash() *kustHash { // Hash returns a hash of the given object func (h *kustHash) Hash(m ifc.Kunstructured) (string, error) { - u := unstructured.Unstructured{ - Object: m.Map(), - } - kind := u.GetKind() - switch kind { - case "ConfigMap": - cm, err := unstructuredToConfigmap(u) - if err != nil { - return "", err - } - return configMapHash(cm) - case "Secret": - sec, err := unstructuredToSecret(u) - if err != nil { - return "", err - } - return secretHash(sec) - default: - return unstructuredHash(&u) - } -} - -// configMapHash returns a hash of the ConfigMap. -// The Data, Kind, and Name are taken into account. -func configMapHash(cm *corev1.ConfigMap) (string, error) { - encoded, err := encodeConfigMap(cm) + node, err := filtersutil.GetRNode(m) if err != nil { return "", err } - h, err := hasher.Encode(hasher.Hash(encoded)) - if err != nil { - return "", err - } - return h, nil -} - -// SecretHash returns a hash of the Secret. -// The Data, Kind, Name, and Type are taken into account. -func secretHash(sec *corev1.Secret) (string, error) { - encoded, err := encodeSecret(sec) - if err != nil { - return "", err - } - h, err := hasher.Encode(hasher.Hash(encoded)) - if err != nil { - return "", err - } - return h, nil -} - -// unstructuredHash creates a hash for an arbitrary type. -// All fields of the object are taken into account when generating the hash. -// This is a fallback for when a specalised hash for the type is unavailable. -func unstructuredHash(u *unstructured.Unstructured) (string, error) { - encoded, err := json.Marshal(u.Object) - if err != nil { - return "", err - } - h, err := hasher.Encode(hasher.Hash(string(encoded))) - if err != nil { - return "", err - } - return h, nil -} - -// encodeConfigMap encodes a ConfigMap. -// Data, Kind, and Name are taken into account. -// BinaryData is included if it's not empty to avoid useless key in output. -func encodeConfigMap(cm *corev1.ConfigMap) (string, error) { - // json.Marshal sorts the keys in a stable order in the encoding - m := map[string]interface{}{"kind": "ConfigMap", "name": cm.Name, "data": cm.Data} - if len(cm.BinaryData) > 0 { - m["binaryData"] = cm.BinaryData - } - data, err := json.Marshal(m) - if err != nil { - return "", err - } - return string(data), nil -} - -// encodeSecret encodes a Secret. -// Data, Kind, Name, and Type are taken into account. -// StringData is included if it's not empty to avoid useless key in output. -func encodeSecret(sec *corev1.Secret) (string, error) { - // json.Marshal sorts the keys in a stable order in the encoding - m := map[string]interface{}{"kind": "Secret", "type": sec.Type, "name": sec.Name, "data": sec.Data} - if len(sec.StringData) > 0 { - m["stringData"] = sec.StringData - } - data, err := json.Marshal(m) - if err != nil { - return "", err - } - return string(data), nil -} - -func unstructuredToConfigmap(u unstructured.Unstructured) (*corev1.ConfigMap, error) { - marshaled, err := json.Marshal(u.Object) - if err != nil { - return nil, err - } - var out corev1.ConfigMap - err = json.Unmarshal(marshaled, &out) - return &out, err -} - -func unstructuredToSecret(u unstructured.Unstructured) (*corev1.Secret, error) { - marshaled, err := json.Marshal(u.Object) - if err != nil { - return nil, err - } - var out corev1.Secret - err = json.Unmarshal(marshaled, &out) - return &out, err + return hasher.HashRNode(node) } diff --git a/api/k8sdeps/kunstruct/hasher_test.go b/api/k8sdeps/kunstruct/hasher_test.go deleted file mode 100644 index be84ad382..000000000 --- a/api/k8sdeps/kunstruct/hasher_test.go +++ /dev/null @@ -1,224 +0,0 @@ -// Copyright 2019 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package kunstruct - -import ( - "reflect" - "strings" - "testing" - - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -func TestConfigMapHash(t *testing.T) { - cases := []struct { - desc string - cm *corev1.ConfigMap - hash string - err string - }{ - // empty map - {"empty data", &corev1.ConfigMap{Data: map[string]string{}, BinaryData: map[string][]byte{}}, "42745tchd9", ""}, - // one key - {"one key", &corev1.ConfigMap{Data: map[string]string{"one": ""}}, "9g67k2htb6", ""}, - // three keys (tests sorting order) - {"three keys", &corev1.ConfigMap{Data: map[string]string{"two": "2", "one": "", "three": "3"}}, "f5h7t85m9b", ""}, - // empty binary data map - {"empty binary data", &corev1.ConfigMap{BinaryData: map[string][]byte{}}, "dk855m5d49", ""}, - // one key with binary data - {"one key with binary data", &corev1.ConfigMap{BinaryData: map[string][]byte{"one": []byte("")}}, "mk79584b8c", ""}, - // three keys with binary data (tests sorting order) - {"three keys with binary data", &corev1.ConfigMap{BinaryData: map[string][]byte{"two": []byte("2"), "one": []byte(""), "three": []byte("3")}}, "t458mc6db2", ""}, - // two keys, one with string and another with binary data - {"two keys with one each", &corev1.ConfigMap{Data: map[string]string{"one": ""}, BinaryData: map[string][]byte{"two": []byte("")}}, "698h7c7t9m", ""}, - } - - for _, c := range cases { - h, err := configMapHash(c.cm) - if SkipRest(t, c.desc, err, c.err) { - continue - } - if c.hash != h { - t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h) - } - } -} - -func TestSecretHash(t *testing.T) { - cases := []struct { - desc string - secret *corev1.Secret - hash string - err string - }{ - // empty map - {"empty data", &corev1.Secret{Type: "my-type", Data: map[string][]byte{}}, "t75bgf6ctb", ""}, - // one key - {"one key", &corev1.Secret{Type: "my-type", Data: map[string][]byte{"one": []byte("")}}, "74bd68bm66", ""}, - // three keys (tests sorting order) - {"three keys", &corev1.Secret{Type: "my-type", Data: map[string][]byte{"two": []byte("2"), "one": []byte(""), "three": []byte("3")}}, "dgcb6h9tmk", ""}, - // with stringdata - {"stringdata", &corev1.Secret{Type: "my-type", Data: map[string][]byte{"one": []byte("")}, StringData: map[string]string{"two": "2"}}, "ckm7f798g2", ""}, - // empty stringdata - {"empty stringdata", &corev1.Secret{Type: "my-type", Data: map[string][]byte{"one": []byte("")}, StringData: map[string]string{}}, "74bd68bm66", ""}, - } - - for _, c := range cases { - h, err := secretHash(c.secret) - if SkipRest(t, c.desc, err, c.err) { - continue - } - if c.hash != h { - t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h) - } - } -} - -func TestUnstructuredHash(t *testing.T) { - cases := []struct { - desc string - unstructured *unstructured.Unstructured - hash string - err string - }{ - {"minimal", &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "test/v1", - "kind": "TestResource", - "metadata": map[string]string{"name": "my-resource"}}, - }, "2tt46d7f79", ""}, - {"with spec", &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "test/v1", - "kind": "TestResource", - "metadata": map[string]string{"name": "my-resource"}, - "spec": map[string]interface{}{"foo": 1, "bar": "abc"}}, - }, "6gc62g4m6k", ""}, - } - - for _, c := range cases { - h, err := unstructuredHash(c.unstructured) - if SkipRest(t, c.desc, err, c.err) { - continue - } - if c.hash != h { - t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h) - } - } -} - -func TestEncodeConfigMap(t *testing.T) { - cases := []struct { - desc string - cm *corev1.ConfigMap - expect string - err string - }{ - // empty map - {"empty data", &corev1.ConfigMap{Data: map[string]string{}}, `{"data":{},"kind":"ConfigMap","name":""}`, ""}, - // one key - {"one key", &corev1.ConfigMap{Data: map[string]string{"one": ""}}, `{"data":{"one":""},"kind":"ConfigMap","name":""}`, ""}, - // three keys (tests sorting order) - {"three keys", &corev1.ConfigMap{Data: map[string]string{"two": "2", "one": "", "three": "3"}}, - `{"data":{"one":"","three":"3","two":"2"},"kind":"ConfigMap","name":""}`, ""}, - // empty binary map - {"empty data", &corev1.ConfigMap{BinaryData: map[string][]byte{}}, `{"data":null,"kind":"ConfigMap","name":""}`, ""}, - // one key with binary data - {"one key", &corev1.ConfigMap{BinaryData: map[string][]byte{"one": []byte("")}}, - `{"binaryData":{"one":""},"data":null,"kind":"ConfigMap","name":""}`, ""}, - // three keys with binary data (tests sorting order) - {"three keys", &corev1.ConfigMap{BinaryData: map[string][]byte{"two": []byte("2"), "one": []byte(""), "three": []byte("3")}}, - `{"binaryData":{"one":"","three":"Mw==","two":"Mg=="},"data":null,"kind":"ConfigMap","name":""}`, ""}, - // two keys, one string and one binary values - {"two keys with one each", &corev1.ConfigMap{Data: map[string]string{"one": ""}, BinaryData: map[string][]byte{"two": []byte("")}}, - `{"binaryData":{"two":""},"data":{"one":""},"kind":"ConfigMap","name":""}`, ""}, - } - for _, c := range cases { - s, err := encodeConfigMap(c.cm) - if SkipRest(t, c.desc, err, c.err) { - continue - } - if s != c.expect { - t.Errorf("case %q, expect %q but got %q from encode %#v", c.desc, c.expect, s, c.cm) - } - } -} - -func TestEncodeSecret(t *testing.T) { - cases := []struct { - desc string - secret *corev1.Secret - expect string - err string - }{ - // empty map - {"empty data", &corev1.Secret{Type: "my-type", Data: map[string][]byte{}}, `{"data":{},"kind":"Secret","name":"","type":"my-type"}`, ""}, - // one key - {"one key", &corev1.Secret{Type: "my-type", Data: map[string][]byte{"one": []byte("")}}, `{"data":{"one":""},"kind":"Secret","name":"","type":"my-type"}`, ""}, - // three keys (tests sorting order) - note json.Marshal base64 encodes the values because they come in as []byte - {"three keys", &corev1.Secret{ - Type: "my-type", - Data: map[string][]byte{"two": []byte("2"), "one": []byte(""), "three": []byte("3")}, - }, - `{"data":{"one":"","three":"Mw==","two":"Mg=="},"kind":"Secret","name":"","type":"my-type"}`, ""}, - // with stringdata - {"stringdata", &corev1.Secret{Type: "my-type", Data: map[string][]byte{"one": []byte("")}, StringData: map[string]string{"two": "2"}}, - `{"data":{"one":""},"kind":"Secret","name":"","stringData":{"two":"2"},"type":"my-type"}`, ""}, - // empty stringdata - {"empty stringdata", &corev1.Secret{Type: "my-type", Data: map[string][]byte{"one": []byte("")}, StringData: map[string]string{}}, - `{"data":{"one":""},"kind":"Secret","name":"","type":"my-type"}`, ""}, - } - for _, c := range cases { - s, err := encodeSecret(c.secret) - if SkipRest(t, c.desc, err, c.err) { - continue - } - if s != c.expect { - t.Errorf("case %q, expect %q but got %q from encode %#v", c.desc, c.expect, s, c.secret) - } - } -} - -// warn devs who change types that they might have to update a hash function -// not perfect, as it only checks the number of top-level fields -func TestTypeStability(t *testing.T) { - errfmt := `case %q, expected %d fields but got %d -Depending on the field(s) you added, you may need to modify the hash function for this type. -To guide you: the hash function targets fields that comprise the contents of objects, -not their metadata (e.g. the Data of a ConfigMap, but nothing in ObjectMeta). -` - cases := []struct { - typeName string - obj interface{} - expect int - }{ - {"ConfigMap", corev1.ConfigMap{}, 4}, - {"Secret", corev1.Secret{}, 5}, - } - for _, c := range cases { - val := reflect.ValueOf(c.obj) - if num := val.NumField(); c.expect != num { - t.Errorf(errfmt, c.typeName, c.expect, num) - } - } -} - -// SkipRest returns true if there was a non-nil error or if we expected an error that didn't happen, -// and logs the appropriate error on the test object. -// The return value indicates whether we should skip the rest of the test case due to the error result. -func SkipRest(t *testing.T, desc string, err error, contains string) bool { - if err != nil { - if len(contains) == 0 { - t.Errorf("case %q, expect nil error but got %q", desc, err.Error()) - } else if !strings.Contains(err.Error(), contains) { - t.Errorf("case %q, expect error to contain %q but got %q", desc, contains, err.Error()) - } - return true - } else if len(contains) > 0 { - t.Errorf("case %q, expect error to contain %q but got nil error", desc, contains) - return true - } - return false -}