diff --git a/api/k8sdeps/kunstruct/hasher.go b/api/k8sdeps/kunstruct/hasher.go index 35bf5eb220..1b263fede0 100644 --- a/api/k8sdeps/kunstruct/hasher.go +++ b/api/k8sdeps/kunstruct/hasher.go @@ -5,7 +5,6 @@ package kunstruct import ( "encoding/json" - "fmt" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -21,7 +20,7 @@ func NewKustHash() *kustHash { return &kustHash{} } -// Hash returns a hash of either a ConfigMap or a Secret +// Hash returns a hash of the given object func (h *kustHash) Hash(m ifc.Kunstructured) (string, error) { u := unstructured.Unstructured{ Object: m.Map(), @@ -36,15 +35,12 @@ func (h *kustHash) Hash(m ifc.Kunstructured) (string, error) { return configMapHash(cm) case "Secret": sec, err := unstructuredToSecret(u) - if err != nil { return "", err } return secretHash(sec) default: - return "", fmt.Errorf( - "type %s is not supported for hashing in %v", - kind, m.Map()) + return unstructuredHash(&u) } } @@ -76,6 +72,21 @@ func secretHash(sec *corev1.Secret) (string, error) { 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. diff --git a/api/k8sdeps/kunstruct/hasher_test.go b/api/k8sdeps/kunstruct/hasher_test.go index 0c43ec4d95..be84ad3828 100644 --- a/api/k8sdeps/kunstruct/hasher_test.go +++ b/api/k8sdeps/kunstruct/hasher_test.go @@ -9,6 +9,7 @@ import ( "testing" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) func TestConfigMapHash(t *testing.T) { @@ -75,6 +76,39 @@ func TestSecretHash(t *testing.T) { } } +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 diff --git a/site/content/en/guides/plugins/_index.md b/site/content/en/guides/plugins/_index.md index d3d6b970e3..095361844b 100644 --- a/site/content/en/guides/plugins/_index.md +++ b/site/content/en/guides/plugins/_index.md @@ -253,7 +253,11 @@ A generator exec plugin can adjust the generator options for the resources it em Resources can be marked as needing to be processed by the internal hash transformer by including the `needs-hash` annotation. When set valid values for the annotation are `"true"` and `"false"` which respectively enable or disable hash suffixing for the resource. Omitting the annotation is equivalent to setting the value `"false"`. -If this annotation is set on a resource not supported by the hash transformer the build will fail. +Hashes are determined as follows: + +* For `ConfigMap` resources, hashes are based on the values of the `name`, `data`, and `binaryData` fields. +* For `Secret` resources, hashes are based on the values of the `name`, `type`, `data`, and `stringData` fields. +* For any other object type, hashes are based on the entire object content (i.e. all fields). Example: