Skip to content

Commit c37d3a0

Browse files
committed
komega: add unstructured support to EqualObject
1 parent 25e9b08 commit c37d3a0

File tree

2 files changed

+651
-127
lines changed

2 files changed

+651
-127
lines changed

pkg/envtest/komega/equalobject.go

Lines changed: 103 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ package komega
1717
import (
1818
"fmt"
1919
"reflect"
20-
"strconv"
2120
"strings"
2221

2322
"github.com/google/go-cmp/cmp"
@@ -33,15 +32,15 @@ var (
3332
// IgnoreAutogeneratedMetadata contains the paths for all the metadata fields that are commonly set by the
3433
// client and APIServer. This is used as a MatchOption for situations when only user-provided metadata is relevant.
3534
IgnoreAutogeneratedMetadata = IgnorePaths{
36-
{"ObjectMeta", "UID"},
37-
{"ObjectMeta", "Generation"},
38-
{"ObjectMeta", "CreationTimestamp"},
39-
{"ObjectMeta", "ResourceVersion"},
40-
{"ObjectMeta", "ManagedFields"},
41-
{"ObjectMeta", "DeletionGracePeriodSeconds"},
42-
{"ObjectMeta", "DeletionTimestamp"},
43-
{"ObjectMeta", "SelfLink"},
44-
{"ObjectMeta", "GenerateName"},
35+
"metadata.uid",
36+
"metadata.generation",
37+
"metadata.creationTimestamp",
38+
"metadata.resourceVersion",
39+
"metadata.managedFields",
40+
"metadata.deletionGracePeriodSeconds",
41+
"metadata.deletionTimestamp",
42+
"metadata.selfLink",
43+
"metadata.generateName",
4544
}
4645
)
4746

@@ -112,76 +111,67 @@ func (d diffPath) String() string {
112111
// diffReporter is a custom recorder for cmp.Diff which records all paths that are
113112
// different between two objects.
114113
type diffReporter struct {
115-
stack []cmp.PathStep
116-
path []string
117-
jsonPath []string
114+
stack []cmp.PathStep
118115

119116
diffPaths []diffPath
120117
}
121118

122119
func (r *diffReporter) PushStep(s cmp.PathStep) {
123120
r.stack = append(r.stack, s)
124-
if len(r.stack) <= 1 {
125-
return
126-
}
127-
switch s := s.(type) {
128-
case cmp.SliceIndex:
129-
r.path = append(r.path, strconv.Itoa(s.Key()))
130-
r.jsonPath = append(r.jsonPath, strconv.Itoa(s.Key()))
131-
case cmp.MapIndex:
132-
key := fmt.Sprintf("%v", s.Key())
133-
// if strings.ContainsAny(key, ".[]/\\") {
134-
// key = fmt.Sprintf("[%s]", key)
135-
// } else {
136-
// key = "." + key
137-
// }
138-
r.path = append(r.path, key)
139-
r.jsonPath = append(r.jsonPath, key)
140-
case cmp.StructField:
141-
field := r.stack[len(r.stack)-2].Type().Field(s.Index())
142-
jsonName := strings.Split(field.Tag.Get("json"), ",")[0]
143-
r.path = append(r.path, s.String()[1:])
144-
r.jsonPath = append(r.jsonPath, jsonName)
145-
}
146121
}
147122

148123
func (r *diffReporter) Report(res cmp.Result) {
149124
if !res.Equal() {
150-
r.diffPaths = append(r.diffPaths, diffPath{types: r.path, json: r.jsonPath})
125+
r.diffPaths = append(r.diffPaths, r.currentPath())
151126
}
152127
}
153128

154-
// func (r *diffReporter) currPath() string {
155-
// p := []string{}
156-
// for _, s := range r.stack[1:] {
157-
// switch s := s.(type) {
158-
// case cmp.StructField, cmp.SliceIndex, cmp.MapIndex:
159-
// p = append(p, s.String())
160-
// }
161-
// }
162-
// return strings.Join(p, "")[1:]
163-
// }
129+
// currentPath converts the current stack into string representations that match
130+
// the IgnorePaths and MatchPaths syntax.
131+
func (r *diffReporter) currentPath() diffPath {
132+
p := diffPath{types: []string{""}, json: []string{""}}
133+
for si, s := range r.stack[1:] {
134+
switch s := s.(type) {
135+
case cmp.StructField:
136+
p.types = append(p.types, s.String()[1:])
137+
// fetch the type information from the parent struct.
138+
// Note: si has an offset of 1 compared to r.stack as we loop over r.stack[1:], so we don't need -1
139+
field := r.stack[si].Type().Field(s.Index())
140+
p.json = append(p.json, strings.Split(field.Tag.Get("json"), ",")[0])
141+
case cmp.SliceIndex:
142+
key := fmt.Sprintf("[%d]", s.Key())
143+
p.types[len(p.types)-1] += key
144+
p.json[len(p.json)-1] += key
145+
case cmp.MapIndex:
146+
key := fmt.Sprintf("%v", s.Key())
147+
if strings.ContainsAny(key, ".[]/\\") {
148+
key = fmt.Sprintf("[%s]", key)
149+
p.types[len(p.types)-1] += key
150+
p.json[len(p.json)-1] += key
151+
} else {
152+
p.types = append(p.types, key)
153+
p.json = append(p.json, key)
154+
}
155+
}
156+
}
157+
// Empty strings were added as the first element. If they're still empty, remove them again.
158+
if len(p.json) > 0 && len(p.json[0]) == 0 {
159+
p.json = p.json[1:]
160+
p.types = p.types[1:]
161+
}
162+
return p
163+
}
164164

165165
func (r *diffReporter) PopStep() {
166-
popped := r.stack[len(r.stack)-1]
167166
r.stack = r.stack[:len(r.stack)-1]
168-
if _, ok := popped.(cmp.Indirect); ok {
169-
return
170-
}
171-
if len(r.stack) <= 1 {
172-
return
173-
}
174-
switch popped.(type) {
175-
case cmp.SliceIndex, cmp.MapIndex, cmp.StructField:
176-
r.path = r.path[:len(r.path)-1]
177-
r.jsonPath = r.jsonPath[:len(r.jsonPath)-1]
178-
}
179167
}
180168

181169
// calculateDiff calculates the difference between two objects and returns the
182170
// paths of the fields that do not match.
183171
func (m *equalObjectMatcher) calculateDiff(actual interface{}) []diffPath {
184172
var original interface{} = m.original
173+
// Remove the wrapping Object from unstructured.Unstructured to make comparison behave similar to
174+
// regular objects.
185175
if u, isUnstructured := actual.(*unstructured.Unstructured); isUnstructured {
186176
actual = u.Object
187177
}
@@ -196,33 +186,47 @@ func (m *equalObjectMatcher) calculateDiff(actual interface{}) []diffPath {
196186
// filterDiffPaths filters the diff paths using the paths in EqualObjectOptions.
197187
func filterDiffPaths(opts EqualObjectOptions, paths []diffPath) []diffPath {
198188
result := []diffPath{}
199-
for _, c := range paths {
200-
if len(opts.matchPaths) > 0 && (!matchesAnyPath(c.types, opts.matchPaths) || !matchesAnyPath(c.json, opts.matchPaths)) {
189+
190+
for _, p := range paths {
191+
if len(opts.matchPaths) > 0 && !hasAnyPathPrefix(p, opts.matchPaths) {
201192
continue
202193
}
203-
if matchesAnyPath(c.types, opts.ignorePaths) || matchesAnyPath(c.json, opts.ignorePaths) {
194+
if hasAnyPathPrefix(p, opts.ignorePaths) {
204195
continue
205196
}
206-
result = append(result, c)
197+
198+
result = append(result, p)
207199
}
200+
208201
return result
209202
}
210203

211-
func matchesPath(path []string, prefix []string) bool {
204+
// hasPathPrefix compares the segments of a path.
205+
func hasPathPrefix(path []string, prefix []string) bool {
212206
for i, p := range prefix {
213-
if i >= len(path) || p != path[i] {
207+
if i >= len(path) {
208+
return false
209+
}
210+
// return false if a segment doesn't match
211+
if path[i] != p && (i < len(prefix)-1 || !segmentHasPrefix(path[i], p)) {
214212
return false
215213
}
216214
}
217215
return true
218216
}
219217

220-
// matchesAnyPath returns true if path matches any of the path prefixes.
218+
func segmentHasPrefix(s, prefix string) bool {
219+
return len(s) >= len(prefix) && s[0:len(prefix)] == prefix &&
220+
// if it is a prefix match, make sure the next character is a [ for array/map access
221+
(len(s) == len(prefix) || s[len(prefix)] == '[')
222+
}
223+
224+
// hasAnyPathPrefix returns true if path matches any of the path prefixes.
221225
// It respects the name boundaries within paths, so 'ObjectMeta.Name' does not
222226
// match 'ObjectMeta.Namespace' for example.
223-
func matchesAnyPath(path []string, prefixes [][]string) bool {
227+
func hasAnyPathPrefix(path diffPath, prefixes [][]string) bool {
224228
for _, prefix := range prefixes {
225-
if matchesPath(path, prefix) {
229+
if hasPathPrefix(path.types, prefix) || hasPathPrefix(path.json, prefix) {
226230
return true
227231
}
228232
}
@@ -249,23 +253,46 @@ func (o *EqualObjectOptions) ApplyOptions(opts []EqualObjectOption) *EqualObject
249253
return o
250254
}
251255

252-
// func parsePath(path string) []string {
253-
// s := strings.Split(path, ".")
254-
// return s
255-
// }
256-
257256
// IgnorePaths instructs the Matcher to ignore given paths when computing a diff.
258-
type IgnorePaths [][]string
257+
// Paths are written in a syntax similar to Go with a few special cases. Both types and
258+
// json/yaml field names are supported.
259+
//
260+
// Regular Paths
261+
// "ObjectMeta.Name"
262+
// "metadata.name"
263+
// Arrays
264+
// "metadata.ownerReferences[0].name"
265+
// Maps, if they do not contain any of .[]/\
266+
// "metadata.labels.something"
267+
// Maps, if they contain any of .[]/\
268+
// "metadata.labels[kubernetes.io/something]"
269+
type IgnorePaths []string
259270

260271
// ApplyToEqualObjectMatcher applies this configuration to the given MatchOptions.
261272
func (i IgnorePaths) ApplyToEqualObjectMatcher(opts *EqualObjectOptions) {
262-
opts.ignorePaths = append(opts.ignorePaths, i...)
273+
for _, p := range i {
274+
opts.ignorePaths = append(opts.ignorePaths, strings.Split(p, "."))
275+
}
263276
}
264277

265278
// MatchPaths instructs the Matcher to restrict its diff to the given paths. If empty the Matcher will look at all paths.
266-
type MatchPaths [][]string
279+
// Paths are written in a syntax similar to Go with a few special cases. Both types and
280+
// json/yaml field names are supported.
281+
//
282+
// Regular Paths
283+
// "ObjectMeta.Name"
284+
// "metadata.name"
285+
// Arrays
286+
// "metadata.ownerReferences[0].name"
287+
// Maps, if they do not contain any of .[]/\
288+
// "metadata.labels.something"
289+
// Maps, if they contain any of .[]/\
290+
// "metadata.labels[kubernetes.io/something]"
291+
type MatchPaths []string
267292

268293
// ApplyToEqualObjectMatcher applies this configuration to the given MatchOptions.
269294
func (i MatchPaths) ApplyToEqualObjectMatcher(opts *EqualObjectOptions) {
270-
opts.matchPaths = append(opts.matchPaths, i...)
295+
for _, p := range i {
296+
opts.matchPaths = append(opts.ignorePaths, strings.Split(p, "."))
297+
}
271298
}

0 commit comments

Comments
 (0)