Skip to content

Commit 083ce27

Browse files
committed
Add field path pattern matching
1 parent c68c9ee commit 083ce27

File tree

2 files changed

+253
-4
lines changed

2 files changed

+253
-4
lines changed

Diff for: fieldpath/set.go

+160-4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package fieldpath
1818

1919
import (
20+
"fmt"
2021
"sort"
2122
"strings"
2223

@@ -136,6 +137,111 @@ func (s *Set) EnsureNamedFieldsAreMembers(sc *schema.Schema, tr schema.TypeRef)
136137
}
137138
}
138139

140+
// MustPrefixPattern is the same as PrefixPattern except it panics if parts can't be
141+
// turned into a SetPattern.
142+
func MustPrefixPattern(parts ...interface{}) *SetPattern {
143+
result, err := PrefixPattern(parts...)
144+
if err != nil {
145+
panic(err)
146+
}
147+
return result
148+
}
149+
150+
// PrefixPattern creates a SetPattern that matches all field paths prefixed by the given list of path parts.
151+
// The parts may be PathPatterns, PathElements, strings (for field names) or ints (for array indices).
152+
// `MatchAnyPathElement()` may be used to "wildcard" match any PathElement at that position in the field path.
153+
func PrefixPattern(parts ...interface{}) (*SetPattern, error) {
154+
current := MatchAnySet() // match all field patch suffixes
155+
for i := len(parts) - 1; i >= 0; i-- {
156+
part := parts[i]
157+
var pattern PathPattern
158+
switch t := part.(type) {
159+
case PathPattern:
160+
pattern = t
161+
case PathElement:
162+
pattern = PathPattern{PathElement: t}
163+
case string:
164+
pattern = PathPattern{PathElement: PathElement{FieldName: &t}}
165+
case int:
166+
pattern = PathPattern{PathElement: PathElement{Index: &t}}
167+
default:
168+
return nil, fmt.Errorf("unexpected type %T", t)
169+
}
170+
current = &SetPattern{
171+
Members: []*MemberSetPattern{{
172+
Path: pattern,
173+
Child: current,
174+
}},
175+
}
176+
}
177+
return current, nil
178+
}
179+
180+
// MatchAnyPathElement returns a PathPattern that matches any path element.
181+
func MatchAnyPathElement() PathPattern {
182+
return PathPattern{Wildcard: true}
183+
}
184+
185+
// MatchAnySet returns a SetPattern that matches any set.
186+
func MatchAnySet() *SetPattern {
187+
return &SetPattern{Wildcard: true}
188+
}
189+
190+
// SetPattern defines a pattern that matches fields in a Set.
191+
// SetPattern is structured much like a Set but with wildcard support.
192+
type SetPattern struct {
193+
// Wildcard indicates that all members and children are matched.
194+
// If set, the Members and Children fields are ignored.
195+
Wildcard bool
196+
// Members provides patterns to match the Members of a Set.
197+
// If any PatchPattern is a wildcard, then all members of a Set are matched.
198+
// Otherwise, if any PathPattern is Equal to a member of a Set, that member is matched.
199+
Members []*MemberSetPattern
200+
}
201+
202+
// MemberSetPattern defines a pattern that matches the Members of a Set.
203+
// MemberSetPattern is structured much like the elements of a SetNodeMap, but with wildcard support.
204+
type MemberSetPattern struct {
205+
// Path provides a pattern to match Members of a Set.
206+
// If Path is a wildcard, all Members of a Set are matched.
207+
// Otherwise, the Member of a Set with a path that is Equal to this Path is matched.
208+
Path PathPattern
209+
210+
// Child provides a pattern to use for Member of a Set that were matched by this MemberSetPattern's Path.
211+
Child *SetPattern
212+
}
213+
214+
// PathPattern defined a match pattern for a PathElement.
215+
type PathPattern struct {
216+
// Wildcard indicates that all PathElements are matched by this pattern.
217+
// If set, PathElement is ignored.
218+
Wildcard bool
219+
220+
// PathElement matches another PathElement if it is Equal to this PathElement.
221+
PathElement
222+
}
223+
224+
// FilterByPattern returns a Set with only fields that match the pattern.
225+
func (s *Set) FilterByPattern(pattern *SetPattern) *Set {
226+
if pattern.Wildcard {
227+
return s
228+
}
229+
230+
members := PathElementSet{}
231+
for _, m := range s.Members.members {
232+
for _, pm := range pattern.Members {
233+
if pm.Path.Wildcard || pm.Path.PathElement.Equals(m) {
234+
members.Insert(m)
235+
break
236+
}
237+
}
238+
}
239+
return &Set{
240+
Members: members,
241+
Children: *s.Children.FilterByPattern(pattern),
242+
}
243+
}
244+
139245
// Size returns the number of members of the set.
140246
func (s *Set) Size() int {
141247
return s.Members.Size() + s.Children.Size()
@@ -476,6 +582,33 @@ func (s *SetNodeMap) EnsureNamedFieldsAreMembers(sc *schema.Schema, tr schema.Ty
476582
}
477583
}
478584

585+
// FilterByPattern returns a set that is filtered by the pattern.
586+
func (s *SetNodeMap) FilterByPattern(pattern *SetPattern) *SetNodeMap {
587+
if pattern.Wildcard {
588+
return s
589+
}
590+
591+
var out sortedSetNode
592+
for _, member := range s.members {
593+
for _, c := range pattern.Members {
594+
if c.Path.Wildcard || c.Path.PathElement.Equals(member.pathElement) {
595+
childSet := member.set.FilterByPattern(c.Child)
596+
if childSet.Size() > 0 {
597+
out = append(out, setNode{
598+
pathElement: member.pathElement,
599+
set: childSet,
600+
})
601+
}
602+
break
603+
}
604+
}
605+
}
606+
607+
return &SetNodeMap{
608+
members: out,
609+
}
610+
}
611+
479612
// Iterate calls f for each PathElement in the set.
480613
func (s *SetNodeMap) Iterate(f func(PathElement)) {
481614
for _, n := range s.members {
@@ -504,14 +637,23 @@ func (s *SetNodeMap) Leaves() *SetNodeMap {
504637
return out
505638
}
506639

640+
// Filter defines an interface for filtering Set.
641+
// NewExcludeFilter can be used to create a filter that removes fields at the
642+
// excluded field paths.
643+
// NewPatternFilter can be used to create a filter that removes all fields except
644+
// the fields that match a field path pattern. PrefixPattern and MustPrefixPattern
645+
// can help create field path patterns.
507646
type Filter interface {
647+
// Filter returns a filtered copy of the set.
508648
Filter(*Set) *Set
509649
}
510650

511-
func NewExcludeFilter(set *Set) Filter {
512-
return excludeFilter{set}
651+
// NewExcludeFilter returns a filter that removes field paths in the exclude set.
652+
func NewExcludeFilter(exclude *Set) Filter {
653+
return excludeFilter{exclude}
513654
}
514655

656+
// NewExcludeFilterMap converts a map of APIVersion to exclude set to a map of APIVersion to exclude filters.
515657
func NewExcludeFilterMap(resetFields map[APIVersion]*Set) map[APIVersion]Filter {
516658
result := make(map[APIVersion]Filter)
517659
for k, v := range resetFields {
@@ -521,9 +663,23 @@ func NewExcludeFilterMap(resetFields map[APIVersion]*Set) map[APIVersion]Filter
521663
}
522664

523665
type excludeFilter struct {
524-
exeludeSet *Set
666+
excludeSet *Set
525667
}
526668

527669
func (t excludeFilter) Filter(set *Set) *Set {
528-
return set.RecursiveDifference(t.exeludeSet)
670+
return set.RecursiveDifference(t.excludeSet)
671+
}
672+
673+
// NewPatternFilter returns a filter that only includes field paths that match the pattern.
674+
// PrefixPattern and MustPrefixPattern can help create basic SetPatterns.
675+
func NewPatternFilter(pattern *SetPattern) Filter {
676+
return patternFilter{pattern}
677+
}
678+
679+
type patternFilter struct {
680+
pattern *SetPattern
681+
}
682+
683+
func (pf patternFilter) Filter(set *Set) *Set {
684+
return set.FilterByPattern(pf.pattern)
529685
}

Diff for: fieldpath/set_test.go

+93
Original file line numberDiff line numberDiff line change
@@ -755,3 +755,96 @@ func TestSetNodeMapIterate(t *testing.T) {
755755
}
756756
}
757757
}
758+
759+
func TestFilterByPattern(t *testing.T) {
760+
testCases := []struct {
761+
name string
762+
input *Set
763+
expect *Set
764+
}{
765+
{
766+
name: "exact match",
767+
input: NewSet(
768+
MakePathOrDie("spec"),
769+
MakePathOrDie("spec", "containers"),
770+
MakePathOrDie("spec", "containers", 0, "resources"),
771+
MakePathOrDie("spec", "containers", 0, "resources", "limits"),
772+
MakePathOrDie("spec", "containers", 0, "resources", "limits", "cpu"),
773+
MakePathOrDie("spec", "containers", 0, "resources", "requests"),
774+
MakePathOrDie("spec", "containers", 0, "resources", "requests", "cpu"),
775+
MakePathOrDie("spec", "containers", 0, "resources", "claims"),
776+
MakePathOrDie("spec", "containers", 0, "resources", "claims", 0, "name"),
777+
MakePathOrDie("spec", "containers", 0, "resources", "claims", 0, "request"),
778+
MakePathOrDie("spec", "containers", 0, "resources", "claims", 1, "name"),
779+
MakePathOrDie("spec", "containers", 0, "resources", "claims", 1, "request"),
780+
MakePathOrDie("spec", "containers", 1, "resources"),
781+
MakePathOrDie("spec", "containers", 1, "resources", "limits"),
782+
MakePathOrDie("spec", "containers", 1, "resources", "limits", "cpu"),
783+
),
784+
expect: NewSet(
785+
MakePathOrDie("spec"),
786+
MakePathOrDie("spec", "containers"),
787+
MakePathOrDie("spec", "containers", 0, "resources"),
788+
MakePathOrDie("spec", "containers", 0, "resources", "limits"),
789+
MakePathOrDie("spec", "containers", 0, "resources", "limits", "cpu"),
790+
MakePathOrDie("spec", "containers", 0, "resources", "requests"),
791+
MakePathOrDie("spec", "containers", 0, "resources", "requests", "cpu"),
792+
MakePathOrDie("spec", "containers", 0, "resources", "claims"),
793+
MakePathOrDie("spec", "containers", 0, "resources", "claims", 0, "name"),
794+
MakePathOrDie("spec", "containers", 0, "resources", "claims", 0, "request"),
795+
MakePathOrDie("spec", "containers", 0, "resources", "claims", 1, "name"),
796+
MakePathOrDie("spec", "containers", 0, "resources", "claims", 1, "request"),
797+
MakePathOrDie("spec", "containers", 1, "resources"),
798+
MakePathOrDie("spec", "containers", 1, "resources", "limits"),
799+
MakePathOrDie("spec", "containers", 1, "resources", "limits", "cpu"),
800+
),
801+
},
802+
{
803+
name: "filter status and metadata",
804+
input: NewSet(
805+
MakePathOrDie("spec"),
806+
MakePathOrDie("status"),
807+
MakePathOrDie("metadata"),
808+
),
809+
expect: NewSet(
810+
MakePathOrDie("spec"),
811+
),
812+
},
813+
{
814+
name: "filter non-container spec fields",
815+
input: NewSet(
816+
MakePathOrDie("spec"),
817+
MakePathOrDie("spec", "volumes"),
818+
MakePathOrDie("spec", "hostNetwork"),
819+
),
820+
expect: NewSet(
821+
MakePathOrDie("spec"),
822+
),
823+
},
824+
{
825+
name: "filter non-resource container fields",
826+
input: NewSet(
827+
MakePathOrDie("spec"),
828+
MakePathOrDie("spec", "containers"),
829+
MakePathOrDie("spec", "containers", 0, "image"),
830+
MakePathOrDie("spec", "containers", 0, "workingDir"),
831+
MakePathOrDie("spec", "containers", 0, "resources"),
832+
),
833+
expect: NewSet(
834+
MakePathOrDie("spec"),
835+
MakePathOrDie("spec", "containers"),
836+
MakePathOrDie("spec", "containers", 0, "resources"),
837+
),
838+
},
839+
}
840+
841+
for _, tc := range testCases {
842+
filter := NewPatternFilter(MustPrefixPattern("spec", "containers", MatchAnyPathElement(), "resources"))
843+
t.Run(tc.name, func(t *testing.T) {
844+
filtered := filter.Filter(tc.input)
845+
if !filtered.Equals(tc.expect) {
846+
t.Errorf("Expected:\n%v\n\nbut got:\n%v", tc.expect, filtered)
847+
}
848+
})
849+
}
850+
}

0 commit comments

Comments
 (0)