Skip to content

Commit 4867f92

Browse files
authored
promote rukpak hash util to shared package, use in boxcutter applier (#2201)
Signed-off-by: Joe Lanford <[email protected]>
1 parent c73b6e0 commit 4867f92

File tree

9 files changed

+59
-88
lines changed

9 files changed

+59
-88
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ require (
99
github.com/cert-manager/cert-manager v1.18.2
1010
github.com/containerd/containerd v1.7.28
1111
github.com/containers/image/v5 v5.36.2
12-
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
1312
github.com/fsnotify/fsnotify v1.9.0
1413
github.com/go-logr/logr v1.4.3
1514
github.com/golang-jwt/jwt/v5 v5.3.0
@@ -89,6 +88,7 @@ require (
8988
github.com/containers/storage v1.59.1 // indirect
9089
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect
9190
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
91+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
9292
github.com/distribution/reference v0.6.0 // indirect
9393
github.com/docker/cli v28.4.0+incompatible // indirect
9494
github.com/docker/distribution v2.8.3+incompatible // indirect

internal/operator-controller/applier/boxcutter.go

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,13 @@ package applier
33
import (
44
"cmp"
55
"context"
6-
"crypto/sha256"
7-
"encoding/hex"
86
"errors"
97
"fmt"
10-
"hash"
118
"io/fs"
129
"maps"
1310
"slices"
1411
"strings"
1512

16-
"github.com/davecgh/go-spew/spew"
1713
"helm.sh/helm/v3/pkg/release"
1814
"helm.sh/helm/v3/pkg/storage/driver"
1915
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -33,6 +29,7 @@ import (
3329
"github.com/operator-framework/operator-controller/internal/operator-controller/labels"
3430
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle/source"
3531
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render"
32+
hashutil "github.com/operator-framework/operator-controller/internal/shared/util/hash"
3633
)
3734

3835
const (
@@ -233,7 +230,7 @@ func (bc *Boxcutter) apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
233230
if err != nil {
234231
return false, "", err
235232
}
236-
desiredHash := computeSHA256Hash(desiredRevision.Spec.Phases)
233+
desiredHash := hashutil.DeepHashObject(desiredRevision.Spec.Phases)
237234

238235
// Sort into current and previous revisions.
239236
var (
@@ -355,33 +352,6 @@ func (bc *Boxcutter) getExistingRevisions(ctx context.Context, extName string) (
355352
return existingRevisionList.Items, nil
356353
}
357354

358-
// computeSHA256Hash returns a sha236 hash value calculated from object.
359-
func computeSHA256Hash(obj any) string {
360-
hasher := sha256.New()
361-
deepHashObject(hasher, obj)
362-
return hex.EncodeToString(hasher.Sum(nil))
363-
}
364-
365-
// deepHashObject writes specified object to hash using the spew library
366-
// which follows pointers and prints actual values of the nested objects
367-
// ensuring the hash does not change when a pointer changes.
368-
func deepHashObject(hasher hash.Hash, objectToWrite any) {
369-
hasher.Reset()
370-
371-
// TODO: change this out to `json.Marshal`. Pretty sure we found issues in the past where
372-
// spew would produce different output when internal structures changed without the
373-
// external public API changing.
374-
printer := spew.ConfigState{
375-
Indent: " ",
376-
SortKeys: true,
377-
DisableMethods: true,
378-
SpewKeys: true,
379-
}
380-
if _, err := printer.Fprintf(hasher, "%#v", objectToWrite); err != nil {
381-
panic(err)
382-
}
383-
}
384-
385355
func latestRevisionNumber(prevRevisions []ocv1.ClusterExtensionRevision) int64 {
386356
if len(prevRevisions) == 0 {
387357
return 0

internal/operator-controller/applier/boxcutter_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ func TestBoxcutter_Apply(t *testing.T) {
354354
UID: "test-uid",
355355
},
356356
}
357-
defaultDesiredHash := "705ada5296ab26f74d94bfa497295a0cbccdb140623bbe704a3506cd1dfba4eb"
357+
defaultDesiredHash := "gvvp8nzq5sbila80hkiv69am8hdr7o68qkk8n084gdn"
358358
defaultDesiredRevision := &ocv1.ClusterExtensionRevision{
359359
ObjectMeta: metav1.ObjectMeta{
360360
Name: "test-ext-1",
@@ -546,7 +546,7 @@ func TestBoxcutter_Apply(t *testing.T) {
546546

547547
assert.Equal(t, "test-ext-2", newRev.Name)
548548
assert.Equal(t, int64(2), newRev.Spec.Revision)
549-
assert.Equal(t, "9d0e48f6830fce1be5f510eb996f2876719fdb8bcffcfe1dfd3fd60e56316424", newRev.Annotations[applier.RevisionHashAnnotation])
549+
assert.Equal(t, "1fqrim12vefkogp3pwxwhcs7c0pi1z1t2fw4roxu81sv", newRev.Annotations[applier.RevisionHashAnnotation])
550550
require.Len(t, newRev.Spec.Previous, 1)
551551
assert.Equal(t, "test-ext-1", newRev.Spec.Previous[0].Name)
552552
assert.Equal(t, types.UID("rev-uid-1"), newRev.Spec.Previous[0].UID)

internal/operator-controller/rukpak/render/registryv1/generators/generators.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,7 @@ func BundleCSVPermissionsGenerator(rv1 *bundle.RegistryV1, opts render.Options)
122122
for _, ns := range opts.TargetNamespaces {
123123
for _, permission := range permissions {
124124
saName := saNameOrDefault(permission.ServiceAccountName)
125-
name, err := opts.UniqueNameGenerator(fmt.Sprintf("%s-%s", rv1.CSV.Name, saName), permission)
126-
if err != nil {
127-
return nil, err
128-
}
125+
name := opts.UniqueNameGenerator(fmt.Sprintf("%s-%s", rv1.CSV.Name, saName), permission)
129126

130127
objs = append(objs,
131128
CreateRoleResource(name, ns, WithRules(permission.Rules...)),
@@ -167,10 +164,7 @@ func BundleCSVClusterPermissionsGenerator(rv1 *bundle.RegistryV1, opts render.Op
167164
objs := make([]client.Object, 0, 2*len(clusterPermissions))
168165
for _, permission := range clusterPermissions {
169166
saName := saNameOrDefault(permission.ServiceAccountName)
170-
name, err := opts.UniqueNameGenerator(fmt.Sprintf("%s-%s", rv1.CSV.Name, saName), permission)
171-
if err != nil {
172-
return nil, err
173-
}
167+
name := opts.UniqueNameGenerator(fmt.Sprintf("%s-%s", rv1.CSV.Name, saName), permission)
174168
objs = append(objs,
175169
CreateClusterRoleResource(name, WithRules(permission.Rules...)),
176170
CreateClusterRoleBindingResource(

internal/operator-controller/rukpak/render/registryv1/generators/generators_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -352,8 +352,8 @@ func Test_BundleCSVDeploymentGenerator_FailsOnNil(t *testing.T) {
352352
}
353353

354354
func Test_BundleCSVPermissionsGenerator_Succeeds(t *testing.T) {
355-
fakeUniqueNameGenerator := func(base string, _ interface{}) (string, error) {
356-
return base, nil
355+
fakeUniqueNameGenerator := func(base string, _ interface{}) string {
356+
return base
357357
}
358358

359359
for _, tc := range []struct {
@@ -786,8 +786,8 @@ func Test_BundleCSVPermissionGenerator_FailsOnNil(t *testing.T) {
786786
}
787787

788788
func Test_BundleCSVClusterPermissionsGenerator_Succeeds(t *testing.T) {
789-
fakeUniqueNameGenerator := func(base string, _ interface{}) (string, error) {
790-
return base, nil
789+
fakeUniqueNameGenerator := func(base string, _ interface{}) string {
790+
return base
791791
}
792792

793793
for _, tc := range []struct {

internal/operator-controller/rukpak/render/render.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle"
1414
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util"
15+
hashutil "github.com/operator-framework/operator-controller/internal/shared/util/hash"
1516
)
1617

1718
// BundleValidator validates a RegistryV1 bundle by executing a series of
@@ -54,7 +55,7 @@ func (r ResourceGenerators) ResourceGenerator() ResourceGenerator {
5455
return r.GenerateResources
5556
}
5657

57-
type UniqueNameGenerator func(string, interface{}) (string, error)
58+
type UniqueNameGenerator func(string, interface{}) string
5859

5960
type Options struct {
6061
InstallNamespace string
@@ -140,12 +141,9 @@ func (r BundleRenderer) Render(rv1 bundle.RegistryV1, installNamespace string, o
140141
return objs, nil
141142
}
142143

143-
func DefaultUniqueNameGenerator(base string, o interface{}) (string, error) {
144-
hashStr, err := util.DeepHashObject(o)
145-
if err != nil {
146-
return "", err
147-
}
148-
return util.ObjectNameForBaseAndSuffix(base, hashStr), nil
144+
func DefaultUniqueNameGenerator(base string, o interface{}) string {
145+
hashStr := hashutil.DeepHashObject(o)
146+
return util.ObjectNameForBaseAndSuffix(base, hashStr)
149147
}
150148

151149
func validateTargetNamespaces(rv1 *bundle.RegistryV1, installNamespace string, targetNamespaces []string) error {

internal/operator-controller/rukpak/render/render_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,10 +206,10 @@ func Test_WithUniqueNameGenerator(t *testing.T) {
206206
opts := &render.Options{
207207
UniqueNameGenerator: render.DefaultUniqueNameGenerator,
208208
}
209-
render.WithUniqueNameGenerator(func(s string, i interface{}) (string, error) {
210-
return "a man needs a name", nil
209+
render.WithUniqueNameGenerator(func(s string, i interface{}) string {
210+
return "a man needs a name"
211211
})(opts)
212-
generatedName, _ := opts.UniqueNameGenerator("", nil)
212+
generatedName := opts.UniqueNameGenerator("", nil)
213213
require.Equal(t, "a man needs a name", generatedName)
214214
}
215215

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package util
1+
package hash
22

33
import (
44
"crypto/sha256"
@@ -7,10 +7,10 @@ import (
77
"math/big"
88
)
99

10-
// DeepHashObject writes specified object to hash using the spew library
11-
// which follows pointers and prints actual values of the nested objects
12-
// ensuring the hash does not change when a pointer changes.
13-
func DeepHashObject(obj interface{}) (string, error) {
10+
// DeepHashObject writes specified object to hash based on the object's
11+
// JSON representation. If the object fails JSON marshaling, DeepHashObject
12+
// panics.
13+
func DeepHashObject(obj interface{}) string {
1414
// While the most accurate encoding we could do for Kubernetes objects (runtime.Object)
1515
// would use the API machinery serializers, those operate over entire objects - and
1616
// we often need to operate on snippets. Checking with the experts and the implementation,
@@ -22,18 +22,19 @@ func DeepHashObject(obj interface{}) (string, error) {
2222
// will be encoded
2323

2424
hasher := sha256.New224()
25-
hasher.Reset()
2625
encoder := json.NewEncoder(hasher)
2726
if err := encoder.Encode(obj); err != nil {
28-
return "", fmt.Errorf("couldn't encode object: %w", err)
27+
panic(fmt.Sprintf("couldn't encode object: %v", err))
2928
}
3029

31-
// base62(sha224(bytes)) is a useful hash and encoding for adding the contents of this
30+
// TODO: Investigate whether we can change this to base62(sha224(bytes))
31+
// The main concern with changing the base is that it will cause the hash
32+
// function output to change, which may cause issues with consumers expecting
33+
// a consistent hash.
34+
//
35+
// base36(sha224(bytes)) is a useful hash and encoding for adding the contents of this
3236
// to a Kubernetes identifier or other field which has length and character set requirements
33-
var hash []byte
34-
hash = hasher.Sum(hash)
35-
3637
var i big.Int
37-
i.SetBytes(hash[:])
38-
return i.Text(36), nil
38+
i.SetBytes(hasher.Sum(nil))
39+
return i.Text(36)
3940
}

internal/operator-controller/rukpak/util/hash_test.go renamed to internal/shared/util/hash/hash_test.go

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,30 @@
1-
package util_test
1+
package hash_test
22

33
import (
4+
"errors"
45
"testing"
56

67
"github.com/stretchr/testify/assert"
78
"github.com/stretchr/testify/require"
89

9-
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util"
10+
hashutil "github.com/operator-framework/operator-controller/internal/shared/util/hash"
1011
)
1112

13+
type unmarshalable struct{}
14+
15+
func (u *unmarshalable) MarshalJSON() ([]byte, error) {
16+
return nil, errors.New("unmarshalable")
17+
}
18+
1219
func TestDeepHashObject(t *testing.T) {
1320
tests := []struct {
1421
name string
15-
wantErr bool
22+
wantPanic bool
1623
obj interface{}
1724
expectedHash string
1825
}{
1926
{
20-
name: "populated obj with exported fields",
21-
wantErr: false,
27+
name: "populated obj with exported fields",
2228
obj: struct {
2329
Str string
2430
Num int
@@ -37,8 +43,7 @@ func TestDeepHashObject(t *testing.T) {
3743
expectedHash: "gta1bt5sybll5qjqxdiekmjm7py93glrinmnrfb31fj",
3844
},
3945
{
40-
name: "modified populated obj with exported fields",
41-
wantErr: false,
46+
name: "modified populated obj with exported fields",
4247
obj: struct {
4348
Str string
4449
Num int
@@ -57,8 +62,7 @@ func TestDeepHashObject(t *testing.T) {
5762
expectedHash: "1ftn1z2ieih8hsmi2a8c6mkoef6uodrtn4wtt1qapioh",
5863
},
5964
{
60-
name: "populated obj with unexported fields",
61-
wantErr: false,
65+
name: "populated obj with unexported fields",
6266
obj: struct {
6367
str string
6468
num int
@@ -80,38 +84,42 @@ func TestDeepHashObject(t *testing.T) {
8084
// The JSON encoder requires exported fields or it will generate
8185
// the same hash as a completely empty object
8286
name: "empty obj",
83-
wantErr: false,
8487
obj: struct{}{},
8588
expectedHash: "16jfjhihxbzhfhs1k5mimq740kvioi98pfbea9q6qtf9",
8689
},
8790
{
8891
name: "string a",
89-
wantErr: false,
9092
obj: "a",
9193
expectedHash: "1lu1qv1451mq7gv9upu1cx8ffffi07rel5xvbvvc44dh",
9294
},
9395
{
9496
name: "string b",
95-
wantErr: false,
9697
obj: "b",
9798
expectedHash: "1ija85ah4gd0beltpfhszipkxfyqqxhp94tf2mjfgq61",
9899
},
99100
{
100101
name: "nil obj",
101-
wantErr: false,
102102
obj: nil,
103103
expectedHash: "2im0kl1kwvzn46sr4cdtkvmdzrlurvj51xdzhwdht8l0",
104104
},
105+
{
106+
name: "unmarshalable obj",
107+
obj: &unmarshalable{},
108+
wantPanic: true,
109+
},
105110
}
106111
for _, tc := range tests {
107112
t.Run(tc.name, func(t *testing.T) {
108-
hash, err := util.DeepHashObject(tc.obj)
109-
if tc.wantErr {
110-
require.Error(t, err)
111-
} else {
112-
require.NoError(t, err)
113+
test := func() {
114+
hash := hashutil.DeepHashObject(tc.obj)
113115
assert.Equal(t, tc.expectedHash, hash)
114116
}
117+
118+
if tc.wantPanic {
119+
require.Panics(t, test)
120+
} else {
121+
require.NotPanics(t, test)
122+
}
115123
})
116124
}
117125
}

0 commit comments

Comments
 (0)