Skip to content

Commit 3f94232

Browse files
authored
Fix hash code calculation (#687)
1 parent a0c0a89 commit 3f94232

File tree

8 files changed

+125
-90
lines changed

8 files changed

+125
-90
lines changed

api/v1/coherence_webhook.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2020, 2024, Oracle and/or its affiliates.
2+
* Copyright (c) 2020, 2025, Oracle and/or its affiliates.
33
* Licensed under the Universal Permissive License v 1.0 as shown at
44
* http://oss.oracle.com/licenses/upl.
55
*/
@@ -54,6 +54,12 @@ func (in *Coherence) Default() {
5454
spec.SetReplicas(spec.GetReplicas())
5555
}
5656
SetCommonDefaults(in)
57+
58+
// apply a label with the hash of the spec - ths must be the last action here to make sure that
59+
// any modifications to the spec field are included in the hash
60+
if hash, applied := EnsureCoherenceHashLabel(in); applied {
61+
webhookLogger.Info(fmt.Sprintf("Applied %s label", LabelCoherenceHash), "hash", hash)
62+
}
5763
}
5864

5965
// SetCommonDefaults sets defaults common to both a Job and StatefulSet
@@ -119,12 +125,6 @@ func SetCommonDefaults(in CoherenceResource) {
119125

120126
// apply the Operator version annotation
121127
in.AddAnnotation(AnnotationOperatorVersion, operator.GetVersion())
122-
123-
// apply a label with the hash of the spec - ths must be the last action here to make sure that
124-
// any modifications to the spec field are included in the hash
125-
if hash, applied := EnsureHashLabel(in); applied {
126-
logger.Info(fmt.Sprintf("Applied %s label", LabelCoherenceHash), "hash", hash)
127-
}
128128
}
129129

130130
// The path in this annotation MUST match the const below

api/v1/coherence_webhook_job.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2020, 2024, Oracle and/or its affiliates.
2+
* Copyright (c) 2020, 2025, Oracle and/or its affiliates.
33
* Licensed under the Universal Permissive License v 1.0 as shown at
44
* http://oss.oracle.com/licenses/upl.
55
*/
@@ -67,6 +67,12 @@ func (in *CoherenceJob) Default() {
6767
}
6868

6969
SetCommonDefaults(in)
70+
71+
// apply a label with the hash of the spec - ths must be the last action here to make sure that
72+
// any modifications to the spec field are included in the hash
73+
if hash, applied := EnsureJobHashLabel(in); applied {
74+
webhookLogger.Info(fmt.Sprintf("Applied %s label", LabelCoherenceHash), "hash", hash)
75+
}
7076
}
7177

7278
// The path in this annotation MUST match the const below

api/v1/hasher.go

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2021, 2023, Oracle and/or its affiliates.
2+
* Copyright (c) 2020, 2025, Oracle and/or its affiliates.
33
* Licensed under the Universal Permissive License v 1.0 as shown at
44
* http://oss.oracle.com/licenses/upl.
55
*/
@@ -8,19 +8,34 @@ package v1
88

99
import (
1010
"encoding/binary"
11+
"encoding/json"
1112
"fmt"
12-
"github.com/davecgh/go-spew/spew"
13-
"hash"
1413
"hash/fnv"
1514
"k8s.io/apimachinery/pkg/util/rand"
1615
)
1716

18-
func EnsureHashLabel(c CoherenceResource) (string, bool) {
17+
func EnsureCoherenceHashLabel(c *Coherence) (string, bool) {
1918
labels := c.GetLabels()
2019
if labels == nil {
2120
labels = make(map[string]string)
2221
}
23-
spec := c.GetSpec()
22+
spec := c.Spec
23+
hashNew := ComputeHash(&spec, nil)
24+
hashCurrent, found := labels[LabelCoherenceHash]
25+
if !found || hashCurrent != hashNew {
26+
labels[LabelCoherenceHash] = hashNew
27+
c.SetLabels(labels)
28+
return hashNew, true
29+
}
30+
return hashCurrent, false
31+
}
32+
33+
func EnsureJobHashLabel(c *CoherenceJob) (string, bool) {
34+
labels := c.GetLabels()
35+
if labels == nil {
36+
labels = make(map[string]string)
37+
}
38+
spec := c.Spec
2439
hashNew := ComputeHash(&spec, nil)
2540
hashCurrent, found := labels[LabelCoherenceHash]
2641
if !found || hashCurrent != hashNew {
@@ -33,30 +48,17 @@ func EnsureHashLabel(c CoherenceResource) (string, bool) {
3348

3449
// ComputeHash returns a hash value calculated from Coherence spec and
3550
// The hash will be safe encoded to avoid bad words.
36-
func ComputeHash(template interface{}, collisionCount *int32) string {
37-
podTemplateSpecHasher := fnv.New32a()
38-
DeepHashObject(podTemplateSpecHasher, template)
51+
func ComputeHash(in interface{}, collisionCount *int32) string {
52+
hasher := fnv.New32a()
53+
b, _ := json.Marshal(in)
54+
_, _ = hasher.Write(b)
3955

4056
// Add collisionCount in the hash if it exists.
4157
if collisionCount != nil {
4258
collisionCountBytes := make([]byte, 8)
4359
binary.LittleEndian.PutUint32(collisionCountBytes, uint32(*collisionCount))
44-
_, _ = podTemplateSpecHasher.Write(collisionCountBytes)
60+
_, _ = hasher.Write(collisionCountBytes)
4561
}
4662

47-
return rand.SafeEncodeString(fmt.Sprint(podTemplateSpecHasher.Sum32()))
48-
}
49-
50-
// DeepHashObject writes specified object to hash using the spew library
51-
// which follows pointers and prints actual values of the nested objects
52-
// ensuring the hash does not change when a pointer changes.
53-
func DeepHashObject(hasher hash.Hash, objectToWrite interface{}) {
54-
hasher.Reset()
55-
printer := spew.ConfigState{
56-
Indent: " ",
57-
SortKeys: true,
58-
DisableMethods: true,
59-
SpewKeys: true,
60-
}
61-
_, _ = printer.Fprintf(hasher, "%#v", objectToWrite)
63+
return rand.SafeEncodeString(fmt.Sprint(hasher.Sum32()))
6264
}

api/v1/hasher_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2020, 2024, Oracle and/or its affiliates.
2+
* Copyright (c) 2020, 2025, Oracle and/or its affiliates.
33
* Licensed under the Universal Permissive License v 1.0 as shown at
44
* http://oss.oracle.com/licenses/upl.
55
*/
@@ -26,10 +26,11 @@ func TestHash(t *testing.T) {
2626
Spec: spec,
2727
}
2828

29-
coh.EnsureHashLabel(deployment)
29+
coh.EnsureCoherenceHashLabel(deployment)
3030

3131
// If this test fails you have probably added a new field to CoherenceResourceSpec
3232
// This will break backwards compatibility. This field needs to be added to
3333
// both CoherenceStatefulSetResourceSpec and CoherenceJobResourceSpec instead
34-
g.Expect(deployment.GetLabels()["coherence-hash"]).To(Equal("5cb9fd9f96"))
34+
//g.Expect(deployment.GetLabels()["coherence-hash"]).To(Equal("5cb9fd9f96"))
35+
g.Expect(deployment.GetLabels()["coherence-hash"]).To(Equal("5859f96865"))
3536
}

controllers/coherence_controller.go

Lines changed: 10 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2020, 2024, Oracle and/or its affiliates.
2+
* Copyright (c) 2020, 2025, Oracle and/or its affiliates.
33
* Licensed under the Universal Permissive License v 1.0 as shown at
44
* http://oss.oracle.com/licenses/upl.
55
*/
@@ -208,37 +208,12 @@ func (in *CoherenceReconciler) Reconcile(ctx context.Context, request ctrl.Reque
208208
}
209209

210210
hash := deployment.GetLabels()[coh.LabelCoherenceHash]
211+
storeHash, _ := storage.GetHash()
211212
var desiredResources coh.Resources
212213

213-
storeHash, found := storage.GetHash()
214-
if !found || storeHash != hash || deployment.Status.Phase != coh.ConditionTypeReady {
215-
// Storage state was saved with no hash or a different hash so is not in the desired state
216-
// or the Coherence resource is not in the Ready state
217-
// Create the desired resources the deployment
218-
if desiredResources, err = deployment.CreateKubernetesResources(); err != nil {
219-
return in.HandleErrAndRequeue(ctx, err, nil, fmt.Sprintf(createResourcesFailedMessage, request.Name, request.Namespace, err), in.Log)
220-
}
221-
222-
if found {
223-
// The "storeHash" is not "", so it must have been processed by the Operator (could have been a previous version).
224-
// There was a bug prior to 3.2.8 where the hash was calculated at the wrong point in the defaulting web-hook,
225-
// so the "currentHash" may be wrong, and hence differ from the recalculated "hash".
226-
if deployment.IsBeforeVersion("3.3.0") {
227-
// the AnnotationOperatorVersion annotation was added in the 3.2.8 web-hook, so if it is missing
228-
// the Coherence resource was added or updated prior to 3.2.8
229-
// In this case we just ignore the difference in hash.
230-
// There is an edge case where the Coherence resource could have legitimately been updated whilst
231-
// the Operator and web-hooks were uninstalled. In that case we would ignore the update until another
232-
// update is made. The simplest way for the customer to work around this is to add the
233-
// AnnotationOperatorVersion annotation with some value, which will then be overwritten by the web-hook
234-
// and the Coherence resource will be correctly processes.
235-
desiredResources = storage.GetLatest()
236-
log.Info("Ignoring hash difference for pre-3.2.8 resource", "hash", hash, "store", storeHash)
237-
}
238-
}
239-
} else {
240-
// storage state was saved with the current hash so is already in the desired state
241-
desiredResources = storage.GetLatest()
214+
desiredResources, err = checkCoherenceHash(deployment, storage, log)
215+
if err != nil {
216+
return in.HandleErrAndRequeue(ctx, err, nil, fmt.Sprintf(createResourcesFailedMessage, request.Name, request.Namespace, err), in.Log)
242217
}
243218

244219
// create the result
@@ -283,9 +258,6 @@ func (in *CoherenceReconciler) Reconcile(ctx context.Context, request ctrl.Reque
283258
}
284259
return reconcile.Result{}, fmt.Errorf("one or more secondary resource reconcilers failed to reconcile")
285260
}
286-
//} else {
287-
// log.Info("Skipping updates for Coherence resource, annotation " + coh.AnnotationOperatorIgnore + " is set to true")
288-
//}
289261

290262
// if replica count is zero update the status to Stopped
291263
if deployment.GetReplicas() == 0 {
@@ -340,6 +312,7 @@ func (in *CoherenceReconciler) SetupWithManager(mgr ctrl.Manager, cs clients.Cli
340312
func (in *CoherenceReconciler) GetReconciler() reconcile.Reconciler { return in }
341313

342314
// ensureHashApplied ensures that the hash label is present in the Coherence resource, patching it if required
315+
// Returns true if the hash label was applied to the Coherence resource, or false if the label was already present
343316
func (in *CoherenceReconciler) ensureHashApplied(ctx context.Context, c *coh.Coherence) (bool, error) {
344317
currentHash := ""
345318
labels := c.GetLabels()
@@ -349,14 +322,14 @@ func (in *CoherenceReconciler) ensureHashApplied(ctx context.Context, c *coh.Coh
349322

350323
// Re-fetch the Coherence resource to ensure we have the most recent copy
351324
latest := c.DeepCopy()
352-
hash, _ := coh.EnsureHashLabel(latest)
325+
hash, _ := coh.EnsureCoherenceHashLabel(latest)
353326

354327
if currentHash != hash {
355-
if c.IsBeforeVersion("3.3.0") {
356-
// Before 3.3.0 there was a bug calculating the has in the defaulting web-hook
328+
if c.IsBeforeVersion("3.4.2") {
329+
// Before 3.4.2 there was a bug calculating the hash in the defaulting web-hook
357330
// This would cause the hashes to be different here, when in fact they should not be
358331
// If the Coherence resource being processes has no version annotation, or a version
359-
// prior to 3.3.0 then we return as if the hashes matched
332+
// prior to 3.4.2 then we return as if the hashes matched
360333
if labels == nil {
361334
labels = make(map[string]string)
362335
}

controllers/coherencejob_controller.go

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -160,19 +160,12 @@ func (in *CoherenceJobReconciler) ReconcileDeployment(ctx context.Context, reque
160160
}
161161

162162
hash := deployment.GetLabels()[coh.LabelCoherenceHash]
163+
storeHash, _ := storage.GetHash()
163164
var desiredResources coh.Resources
164165

165-
storeHash, found := storage.GetHash()
166-
if !found || storeHash != hash || status.Phase != coh.ConditionTypeReady {
167-
// Storage state was saved with no hash or a different hash so is not in the desired state
168-
// or the CoherenceJob resource is not in the Ready state
169-
// Create the desired resources the deployment
170-
if desiredResources, err = deployment.CreateKubernetesResources(); err != nil {
171-
return in.HandleErrAndRequeue(ctx, err, nil, fmt.Sprintf(createResourcesFailedMessage, request.Name, request.Namespace, err), in.Log)
172-
}
173-
} else {
174-
// storage state was saved with the current hash so is already in the desired state
175-
desiredResources = storage.GetLatest()
166+
desiredResources, err = checkJobHash(deployment, storage, log)
167+
if err != nil {
168+
return in.HandleErrAndRequeue(ctx, err, nil, fmt.Sprintf(createResourcesFailedMessage, request.Name, request.Namespace, err), in.Log)
176169
}
177170

178171
// create the result
@@ -271,23 +264,23 @@ func (in *CoherenceJobReconciler) SetupWithManager(mgr ctrl.Manager, cs clients.
271264
func (in *CoherenceJobReconciler) GetReconciler() reconcile.Reconciler { return in }
272265

273266
// ensureHashApplied ensures that the hash label is present in the CoherenceJob resource, patching it if required
274-
func (in *CoherenceJobReconciler) ensureHashApplied(ctx context.Context, c coh.CoherenceResource) (bool, error) {
267+
func (in *CoherenceJobReconciler) ensureHashApplied(ctx context.Context, c *coh.CoherenceJob) (bool, error) {
275268
currentHash := ""
276269
labels := c.GetLabels()
277270
if len(labels) > 0 {
278271
currentHash = labels[coh.LabelCoherenceHash]
279272
}
280273

281274
// Re-fetch the CoherenceJob resource to ensure we have the most recent copy
282-
latest := c.DeepCopyResource()
283-
hash, _ := coh.EnsureHashLabel(latest)
275+
latest := c.DeepCopy()
276+
hash, _ := coh.EnsureJobHashLabel(latest)
284277

285278
if currentHash != hash {
286-
if c.IsBeforeVersion("3.2.8") {
287-
// Before 3.2.8 there was a bug calculating the has in the defaulting web-hook
279+
if c.IsBeforeVersion("3.4.2") {
280+
// Before 3.4.2 there was a bug calculating the has in the defaulting web-hook
288281
// This would cause the hashes to be different here, when in fact they should not be
289282
// If the CoherenceJob resource being processes has no version annotation, or a version
290-
// prior to 3.2.8 then we return as if the hashes matched
283+
// prior to 3.4.2 then we return as if the hashes matched
291284
if labels == nil {
292285
labels = make(map[string]string)
293286
}

controllers/common.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright (c) 2020, 2025, Oracle and/or its affiliates.
3+
* Licensed under the Universal Permissive License v 1.0 as shown at
4+
* http://oss.oracle.com/licenses/upl.
5+
*/
6+
7+
package controllers
8+
9+
import (
10+
"github.com/go-logr/logr"
11+
coh "github.com/oracle/coherence-operator/api/v1"
12+
"github.com/oracle/coherence-operator/pkg/utils"
13+
)
14+
15+
func checkCoherenceHash(deployment *coh.Coherence, storage utils.Storage, log logr.Logger) (coh.Resources, error) {
16+
return checkHash(deployment, deployment.Status.Phase, storage, log)
17+
}
18+
19+
func checkJobHash(deployment *coh.CoherenceJob, storage utils.Storage, log logr.Logger) (coh.Resources, error) {
20+
return checkHash(deployment, deployment.Status.Phase, storage, log)
21+
}
22+
23+
func checkHash(deployment coh.CoherenceResource, phase coh.ConditionType, storage utils.Storage, log logr.Logger) (coh.Resources, error) {
24+
hash := deployment.GetLabels()[coh.LabelCoherenceHash]
25+
var desiredResources coh.Resources
26+
var err error
27+
28+
storeHash, found := storage.GetHash()
29+
if !found || storeHash != hash || phase != coh.ConditionTypeReady {
30+
// Storage state was saved with no hash or a different hash so is not in the desired state
31+
// or the Coherence resource is not in the Ready state
32+
// Create the desired resources the deployment
33+
if desiredResources, err = deployment.CreateKubernetesResources(); err != nil {
34+
return desiredResources, err
35+
}
36+
37+
if found {
38+
// The "storeHash" is not "", so it must have been processed by the Operator (could have been a previous version).
39+
// There was a bug prior to 3.4.2 where the hash was calculated at the wrong point in the defaulting web-hook,
40+
// and the has used only a portion of the spec, so the "currentHash" may be wrong, and hence differ from the
41+
// recalculated "hash".
42+
if deployment.IsBeforeVersion("3.4.2") {
43+
// the AnnotationOperatorVersion annotation was added in the 3.2.8 web-hook, so if it is missing
44+
// the Coherence resource was added or updated prior to 3.2.8, or the version is present but is
45+
// prior to 3.4.2. In this case we just ignore the difference in hash.
46+
// There is an edge case where the Coherence resource could have legitimately been updated whilst
47+
// the Operator and web-hooks were uninstalled. In that case we would ignore the update until another
48+
// update is made. The simplest way for the customer to work around this is to add the
49+
// AnnotationOperatorVersion annotation with some value, which will then be overwritten by the web-hook
50+
// and the Coherence resource will be correctly processes.
51+
desiredResources = storage.GetLatest()
52+
log.Info("Ignoring hash difference for pre-3.4.2 resource", "hash", hash, "store", storeHash)
53+
}
54+
}
55+
} else {
56+
// storage state was saved with the current hash so is already in the desired state
57+
desiredResources = storage.GetLatest()
58+
}
59+
return desiredResources, err
60+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ go 1.23.0
55
toolchain go1.23.4
66

77
require (
8-
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
98
github.com/distribution/reference v0.6.0
109
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32
1110
github.com/go-logr/logr v1.4.2
@@ -31,6 +30,7 @@ require (
3130
require (
3231
github.com/beorn7/perks v1.0.1 // indirect
3332
github.com/cespare/xxhash/v2 v2.3.0 // indirect
33+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
3434
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
3535
github.com/evanphx/json-patch v5.9.0+incompatible // indirect
3636
github.com/evanphx/json-patch/v5 v5.9.0 // indirect

0 commit comments

Comments
 (0)