github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/k8s/object_selector.go (about) 1 package k8s 2 3 import ( 4 "fmt" 5 "regexp" 6 7 "github.com/google/go-cmp/cmp" 8 "github.com/pkg/errors" 9 10 "github.com/tilt-dev/tilt/internal/sliceutils" 11 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 12 ) 13 14 // A selector matches an entity if all non-empty selector fields match the corresponding entity fields 15 type ObjectSelector struct { 16 spec v1alpha1.ObjectSelector 17 18 apiVersion *regexp.Regexp 19 kind *regexp.Regexp 20 name *regexp.Regexp 21 namespace *regexp.Regexp 22 } 23 24 func ParseObjectSelector(spec v1alpha1.ObjectSelector) (ObjectSelector, error) { 25 ret := ObjectSelector{spec: spec} 26 var err error 27 28 ret.apiVersion, err = regexp.Compile(spec.APIVersionRegexp) 29 if err != nil { 30 return ObjectSelector{}, errors.Wrap(err, "error parsing apiVersion regexp") 31 } 32 33 ret.kind, err = regexp.Compile(spec.KindRegexp) 34 if err != nil { 35 return ObjectSelector{}, errors.Wrap(err, "error parsing kind regexp") 36 } 37 38 ret.name, err = regexp.Compile(spec.NameRegexp) 39 if err != nil { 40 return ObjectSelector{}, errors.Wrap(err, "error parsing name regexp") 41 } 42 43 ret.namespace, err = regexp.Compile(spec.NamespaceRegexp) 44 if err != nil { 45 return ObjectSelector{}, errors.Wrap(err, "error parsing namespace regexp") 46 } 47 48 return ret, nil 49 } 50 51 var splitOptions = sliceutils.NewEscapeSplitOptions() 52 53 func SelectorStringFromParts(parts []string) string { 54 return sliceutils.EscapeAndJoin(parts, splitOptions) 55 } 56 57 // format is <name:required>:<kind:optional>:<namespace:optional> 58 func SelectorFromString(s string) (ObjectSelector, error) { 59 parts, err := sliceutils.UnescapeAndSplit(s, splitOptions) 60 if err != nil { 61 return ObjectSelector{}, err 62 } 63 if len(s) == 0 { 64 return ObjectSelector{}, fmt.Errorf("selector can't be empty") 65 } 66 if len(parts) == 1 { 67 return NewFullmatchCaseInsensitiveObjectSelector("", "", parts[0], "") 68 } 69 if len(parts) == 2 { 70 return NewFullmatchCaseInsensitiveObjectSelector("", parts[1], parts[0], "") 71 } 72 if len(parts) == 3 { 73 return NewFullmatchCaseInsensitiveObjectSelector("", parts[1], parts[0], parts[2]) 74 } 75 76 return ObjectSelector{}, fmt.Errorf("Too many parts in selector. Selectors must contain between 1 and 3 parts (colon separated), found %d parts in %s", len(parts), s) 77 } 78 79 // TODO(dmiller): this function and newPartialMatchK8sObjectSelector 80 // should be written in to a form that can be used like this 81 // x := re{pattern: name, ignoreCase: true, fullMatch: true} 82 // x.compile() 83 // rather than passing around and mutating regex strings 84 85 // Creates a new ObjectSelector 86 // If an arg is an empty string it will become an empty regex that matches all input 87 // Otherwise the arg must match the input exactly 88 func NewFullmatchCaseInsensitiveObjectSelector(apiVersion string, kind string, name string, namespace string) (ObjectSelector, error) { 89 return ParseObjectSelector(v1alpha1.ObjectSelector{ 90 APIVersionRegexp: exactOrEmptyRegex(apiVersion), 91 KindRegexp: exactOrEmptyRegex(kind), 92 NameRegexp: exactOrEmptyRegex(name), 93 NamespaceRegexp: exactOrEmptyRegex(namespace), 94 }) 95 } 96 97 func makeCaseInsensitive(s string) string { 98 if s == "" { 99 return s 100 } else { 101 return "(?i)" + s 102 } 103 } 104 105 func exactOrEmptyRegex(s string) string { 106 if s != "" { 107 s = fmt.Sprintf("^%s$", makeCaseInsensitive(regexp.QuoteMeta(s))) 108 } 109 return s 110 } 111 112 // Create a selector that matches the Kind. Useful for testing. 113 func MustKindSelector(kind string) ObjectSelector { 114 sel, err := NewFullmatchCaseInsensitiveObjectSelector("", kind, "", "") 115 if err != nil { 116 panic(err) 117 } 118 return sel 119 } 120 121 // Create a selector that matches the Name. Useful for testing. 122 func MustNameSelector(name string) ObjectSelector { 123 sel, err := NewFullmatchCaseInsensitiveObjectSelector("", "", name, "") 124 if err != nil { 125 panic(err) 126 } 127 return sel 128 } 129 130 // Creates a new ObjectSelector 131 // If an arg is an empty string, it will become an empty regex that matches all input 132 // Otherwise the arg will match input from the beginning (prefix matching) 133 func NewPartialMatchObjectSelector(apiVersion string, kind string, name string, namespace string) (ObjectSelector, error) { 134 return ParseObjectSelector(v1alpha1.ObjectSelector{ 135 APIVersionRegexp: makeCaseInsensitive(apiVersion), 136 KindRegexp: makeCaseInsensitive(kind), 137 NameRegexp: makeCaseInsensitive(name), 138 NamespaceRegexp: makeCaseInsensitive(namespace), 139 }) 140 } 141 142 func (o1 ObjectSelector) EqualsSelector(o2 ObjectSelector) bool { 143 return cmp.Equal(o1.spec, o2.spec) 144 } 145 146 func (k ObjectSelector) Matches(e K8sEntity) bool { 147 gvk := e.GVK() 148 return k.apiVersion.MatchString(gvk.GroupVersion().String()) && 149 k.kind.MatchString(gvk.Kind) && 150 k.name.MatchString(e.Name()) && 151 k.namespace.MatchString(e.Namespace().String()) 152 } 153 154 func (k ObjectSelector) ToSpec() v1alpha1.ObjectSelector { 155 return k.spec 156 }