github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/k8s/locator.go (about)

     1  package k8s
     2  
     3  import (
     4  	"fmt"
     5  	"reflect"
     6  
     7  	"github.com/distribution/reference"
     8  	"github.com/pkg/errors"
     9  	v1 "k8s.io/api/core/v1"
    10  	"k8s.io/apimachinery/pkg/runtime"
    11  
    12  	"github.com/tilt-dev/tilt/internal/container"
    13  	"github.com/tilt-dev/tilt/internal/k8s/jsonpath"
    14  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    15  )
    16  
    17  type ImageLocator interface {
    18  	// Checks whether two image locators are the same.
    19  	EqualsImageLocator(other interface{}) bool
    20  
    21  	// Find all the images in this entity.
    22  	Extract(e K8sEntity) ([]reference.Named, error)
    23  
    24  	// Matches the type of this entity.
    25  	MatchesType(e K8sEntity) bool
    26  
    27  	// Find all the images in this entity that match the given selector,
    28  	// and replace them with the injected ref.
    29  	// If the injected ref has a pull policy sibling, set it to the given pull policy.
    30  	//
    31  	// Returns a new entity with the injected ref.  Returns a boolean indicated
    32  	// whether there was at least one successful injection.
    33  	Inject(e K8sEntity, selector container.RefSelector, injectRef reference.Named, policy v1.PullPolicy) (K8sEntity, bool, error)
    34  
    35  	ToSpec() v1alpha1.KubernetesImageLocator
    36  }
    37  
    38  const imagePullPolicyKey = "imagePullPolicy"
    39  
    40  func LocatorMatchesOne(l ImageLocator, entities []K8sEntity) bool {
    41  	for _, e := range entities {
    42  		if l.MatchesType(e) {
    43  			return true
    44  		}
    45  	}
    46  	return false
    47  }
    48  
    49  type JSONPathImageLocator struct {
    50  	selector ObjectSelector
    51  	path     JSONPath
    52  }
    53  
    54  func MustJSONPathImageLocator(selector ObjectSelector, path string) *JSONPathImageLocator {
    55  	locator, err := NewJSONPathImageLocator(selector, path)
    56  	if err != nil {
    57  		panic(err)
    58  	}
    59  	return locator
    60  }
    61  
    62  func NewJSONPathImageLocator(selector ObjectSelector, path string) (*JSONPathImageLocator, error) {
    63  	p, err := NewJSONPath(path)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  	return &JSONPathImageLocator{
    68  		selector: selector,
    69  		path:     p,
    70  	}, nil
    71  }
    72  
    73  func (l *JSONPathImageLocator) ToSpec() v1alpha1.KubernetesImageLocator {
    74  	return v1alpha1.KubernetesImageLocator{
    75  		ObjectSelector: l.selector.ToSpec(),
    76  		Path:           l.path.String(),
    77  	}
    78  }
    79  
    80  func (l *JSONPathImageLocator) EqualsImageLocator(other interface{}) bool {
    81  	otherL, ok := other.(*JSONPathImageLocator)
    82  	if !ok {
    83  		return false
    84  	}
    85  
    86  	return l.path.path == otherL.path.path &&
    87  		l.selector.EqualsSelector(otherL.selector)
    88  }
    89  
    90  func (l *JSONPathImageLocator) MatchesType(e K8sEntity) bool {
    91  	return l.selector.Matches(e)
    92  }
    93  
    94  func (l *JSONPathImageLocator) unpack(e K8sEntity) interface{} {
    95  	if u, ok := e.Obj.(runtime.Unstructured); ok {
    96  		return u.UnstructuredContent()
    97  	}
    98  	return e.Obj
    99  }
   100  
   101  func (l *JSONPathImageLocator) Extract(e K8sEntity) ([]reference.Named, error) {
   102  	if !l.selector.Matches(e) {
   103  		return nil, nil
   104  	}
   105  
   106  	// also look for images in any json paths that were specified for this entity
   107  	images, err := l.path.FindStrings(l.unpack(e))
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  
   112  	result := make([]reference.Named, 0, len(images))
   113  	for _, image := range images {
   114  		ref, err := container.ParseNamed(image)
   115  		if err != nil {
   116  			return nil, errors.Wrapf(err, "error parsing image '%s' at json path '%s'", image, l.path)
   117  		}
   118  		result = append(result, ref)
   119  	}
   120  	return result, nil
   121  }
   122  
   123  func (l *JSONPathImageLocator) Inject(e K8sEntity, selector container.RefSelector, injectRef reference.Named, pullPolicy v1.PullPolicy) (K8sEntity, bool, error) {
   124  	if !l.selector.Matches(e) {
   125  		return e, false, nil
   126  	}
   127  
   128  	modified := false
   129  	err := l.path.VisitStrings(l.unpack(e), func(val jsonpath.Value, str string) error {
   130  		ref, err := container.ParseNamed(str)
   131  		if err != nil {
   132  			return nil
   133  		}
   134  
   135  		if selector.Matches(ref) {
   136  			val.Set(reflect.ValueOf(container.FamiliarString(injectRef)))
   137  			modified = true
   138  
   139  			if pullPolicyVal, ok := val.Sibling(imagePullPolicyKey); ok && pullPolicyVal.CanSet() {
   140  				pullPolicyVal.Set(reflect.ValueOf(string(pullPolicy)))
   141  			}
   142  		}
   143  		return nil
   144  	})
   145  	if err != nil {
   146  		return e, false, err
   147  	}
   148  	return e, modified, nil
   149  }
   150  
   151  var _ ImageLocator = &JSONPathImageLocator{}
   152  
   153  type JSONPathImageObjectLocator struct {
   154  	selector  ObjectSelector
   155  	path      JSONPath
   156  	repoField string
   157  	tagField  string
   158  }
   159  
   160  func MustJSONPathImageObjectLocator(selector ObjectSelector, path, repoField, tagField string) *JSONPathImageObjectLocator {
   161  	locator, err := NewJSONPathImageObjectLocator(selector, path, repoField, tagField)
   162  	if err != nil {
   163  		panic(err)
   164  	}
   165  	return locator
   166  }
   167  
   168  func NewJSONPathImageObjectLocator(selector ObjectSelector, path, repoField, tagField string) (*JSONPathImageObjectLocator, error) {
   169  	p, err := NewJSONPath(path)
   170  	if err != nil {
   171  		return nil, err
   172  	}
   173  	return &JSONPathImageObjectLocator{
   174  		selector:  selector,
   175  		path:      p,
   176  		repoField: repoField,
   177  		tagField:  tagField,
   178  	}, nil
   179  }
   180  
   181  func (l *JSONPathImageObjectLocator) EqualsImageLocator(other interface{}) bool {
   182  	otherL, ok := other.(*JSONPathImageObjectLocator)
   183  	if !ok {
   184  		return false
   185  	}
   186  	return l.path.path == otherL.path.path &&
   187  		l.repoField == otherL.repoField &&
   188  		l.tagField == otherL.tagField &&
   189  		l.selector.EqualsSelector(otherL.selector)
   190  }
   191  
   192  func (l *JSONPathImageObjectLocator) MatchesType(e K8sEntity) bool {
   193  	return l.selector.Matches(e)
   194  }
   195  
   196  func (l *JSONPathImageObjectLocator) unpack(e K8sEntity) interface{} {
   197  	if u, ok := e.Obj.(runtime.Unstructured); ok {
   198  		return u.UnstructuredContent()
   199  	}
   200  	return e.Obj
   201  }
   202  
   203  func (l *JSONPathImageObjectLocator) extractImageFromMap(val jsonpath.Value) (reference.Named, error) {
   204  	m, ok := val.Interface().(map[string]interface{})
   205  	if !ok {
   206  		return nil, fmt.Errorf("May only match maps (json_path=%q)\nGot Type: %s\nGot Value: %s",
   207  			l.path.path, val.Type(), val)
   208  	}
   209  
   210  	repoField, ok := m[l.repoField].(string)
   211  	imageString := ""
   212  	if ok {
   213  		imageString = repoField
   214  	}
   215  
   216  	tagField, ok := m[l.tagField].(string)
   217  	if ok && tagField != "" {
   218  		imageString = fmt.Sprintf("%s:%s", repoField, tagField)
   219  	}
   220  
   221  	return container.ParseNamed(imageString)
   222  }
   223  
   224  func (l *JSONPathImageObjectLocator) Extract(e K8sEntity) ([]reference.Named, error) {
   225  	if !l.selector.Matches(e) {
   226  		return nil, nil
   227  	}
   228  
   229  	result := make([]reference.Named, 0)
   230  	err := l.path.Visit(l.unpack(e), func(val jsonpath.Value) error {
   231  		ref, err := l.extractImageFromMap(val)
   232  		if err != nil {
   233  			return err
   234  		}
   235  		result = append(result, ref)
   236  		return nil
   237  	})
   238  	if err != nil {
   239  		return nil, err
   240  	}
   241  	return result, nil
   242  }
   243  
   244  // pullPolicy is ignored for this injector for now, since it's less standard
   245  // if it turns out there's a demand for this, we can plumb it through to image_object
   246  func (l *JSONPathImageObjectLocator) Inject(e K8sEntity, selector container.RefSelector, injectRef reference.Named, _ v1.PullPolicy) (K8sEntity, bool, error) {
   247  	if !l.selector.Matches(e) {
   248  		return e, false, nil
   249  	}
   250  
   251  	tagged, isTagged := injectRef.(reference.Tagged)
   252  
   253  	modified := false
   254  	err := l.path.Visit(l.unpack(e), func(val jsonpath.Value) error {
   255  		ref, err := l.extractImageFromMap(val)
   256  		if err != nil {
   257  			return err
   258  		}
   259  		if selector.Matches(ref) {
   260  			m := val.Interface().(map[string]interface{})
   261  			m[l.repoField] = reference.FamiliarName(injectRef)
   262  			if isTagged {
   263  				m[l.tagField] = tagged.Tag()
   264  			}
   265  			modified = true
   266  		}
   267  		return nil
   268  	})
   269  	if err != nil {
   270  		return e, false, err
   271  	}
   272  	return e, modified, nil
   273  }
   274  
   275  func (l *JSONPathImageObjectLocator) ToSpec() v1alpha1.KubernetesImageLocator {
   276  	return v1alpha1.KubernetesImageLocator{
   277  		ObjectSelector: l.selector.ToSpec(),
   278  		Path:           l.path.String(),
   279  		Object: &v1alpha1.KubernetesImageObjectDescriptor{
   280  			RepoField: l.repoField,
   281  			TagField:  l.tagField,
   282  		},
   283  	}
   284  }
   285  
   286  var _ ImageLocator = &JSONPathImageObjectLocator{}