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  }