mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-06-12 01:14:22 +00:00
refactor hasher
This commit is contained in:
@@ -8,6 +8,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
|
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SortArrayAndComputeHash sorts a string array and
|
// SortArrayAndComputeHash sorts a string array and
|
||||||
@@ -50,3 +52,141 @@ func Encode(hex string) (string, error) {
|
|||||||
func Hash(data string) string {
|
func Hash(data string) string {
|
||||||
return fmt.Sprintf("%x", sha256.Sum256([]byte(data)))
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
// Copyright 2019 The Kubernetes Authors.
|
// Copyright 2019 The Kubernetes Authors.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package hasher_test
|
package hasher
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
. "sigs.k8s.io/kustomize/api/hasher"
|
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSortArrayAndComputeHash(t *testing.T) {
|
func TestSortArrayAndComputeHash(t *testing.T) {
|
||||||
@@ -39,3 +40,314 @@ func TestHash(t *testing.T) {
|
|||||||
t.Errorf("expected hash %q but got %q", expect, sum)
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,12 +4,9 @@
|
|||||||
package kunstruct
|
package kunstruct
|
||||||
|
|
||||||
import (
|
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/hasher"
|
||||||
"sigs.k8s.io/kustomize/api/ifc"
|
"sigs.k8s.io/kustomize/api/ifc"
|
||||||
|
"sigs.k8s.io/kustomize/kyaml/filtersutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// kustHash computes a hash of an unstructured object.
|
// kustHash computes a hash of an unstructured object.
|
||||||
@@ -22,119 +19,9 @@ func NewKustHash() *kustHash {
|
|||||||
|
|
||||||
// Hash returns a hash of the given object
|
// Hash returns a hash of the given object
|
||||||
func (h *kustHash) Hash(m ifc.Kunstructured) (string, error) {
|
func (h *kustHash) Hash(m ifc.Kunstructured) (string, error) {
|
||||||
u := unstructured.Unstructured{
|
node, err := filtersutil.GetRNode(m)
|
||||||
Object: m.Map(),
|
|
||||||
}
|
|
||||||
kind := u.GetKind()
|
|
||||||
switch kind {
|
|
||||||
case "ConfigMap":
|
|
||||||
cm, err := unstructuredToConfigmap(u)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return configMapHash(cm)
|
return hasher.HashRNode(node)
|
||||||
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)
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user