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

     1  package k8s
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/distribution/reference"
     7  	"github.com/pkg/errors"
     8  	v1 "k8s.io/api/core/v1"
     9  	"k8s.io/apimachinery/pkg/runtime"
    10  
    11  	"github.com/tilt-dev/tilt/internal/container"
    12  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    13  )
    14  
    15  // Iterate through the fields of a k8s entity and
    16  // replace the image pull policy on all images.
    17  func InjectImagePullPolicy(entity K8sEntity, policy v1.PullPolicy) (K8sEntity, error) {
    18  	entity = entity.DeepCopy()
    19  	containers, err := extractContainers(&entity)
    20  	if err != nil {
    21  		return K8sEntity{}, err
    22  	}
    23  
    24  	for _, container := range containers {
    25  		container.ImagePullPolicy = policy
    26  	}
    27  	return entity, nil
    28  }
    29  
    30  // Iterate through the fields of a k8s entity and
    31  // replace a image name with its digest.
    32  //
    33  // policy: The pull policy to set on the replaced image.
    34  //
    35  //	When working with a local k8s cluster, we want to set this to Never,
    36  //	to ensure that k8s fails hard if the image is missing from docker.
    37  //
    38  // Returns: the new entity, whether the image was replaced, and an error.
    39  func InjectImageDigest(entity K8sEntity, selector container.RefSelector, injectRef reference.Named, locators []ImageLocator, matchInEnvVars bool, policy v1.PullPolicy) (K8sEntity, bool, error) {
    40  	entity = entity.DeepCopy()
    41  
    42  	// NOTE(nick): For some reason, if you have a reference with a digest,
    43  	// kubernetes will never find it in the local registry and always tries to do a
    44  	// pull. It's not clear to me why it behaves this way.
    45  	//
    46  	// There is not a simple way to resolve this problem at this level of the
    47  	// API. In some cases, the digest won't matter and the name/tag will be
    48  	// enough. In other cases, the digest will be critical if we don't have good
    49  	// synchronization that the name/tag currently matches the digest.
    50  	//
    51  	// For now, we try to detect this case and push the error up to the caller.
    52  	_, hasDigest := injectRef.(reference.Digested)
    53  	if hasDigest && policy == v1.PullNever {
    54  		return K8sEntity{}, false, fmt.Errorf("INTERNAL TILT ERROR: Cannot set PullNever with digest")
    55  	}
    56  
    57  	replaced := false
    58  
    59  	entity, r, err := injectImageDigestInContainers(entity, selector, injectRef, policy)
    60  	if err != nil {
    61  		return K8sEntity{}, false, err
    62  	}
    63  	if r {
    64  		replaced = true
    65  	}
    66  
    67  	if matchInEnvVars {
    68  		entity, r, err = injectImageDigestInEnvVars(entity, selector, injectRef)
    69  		if err != nil {
    70  			return K8sEntity{}, false, err
    71  		}
    72  		if r {
    73  			replaced = true
    74  		}
    75  	}
    76  
    77  	for _, locator := range locators {
    78  		entity, r, err = locator.Inject(entity, selector, injectRef, policy)
    79  		if err != nil {
    80  			return K8sEntity{}, false, err
    81  		}
    82  		if r {
    83  			replaced = true
    84  		}
    85  	}
    86  
    87  	return entity, replaced, nil
    88  }
    89  
    90  func injectImageDigestInContainers(entity K8sEntity, selector container.RefSelector, injectRef reference.Named, policy v1.PullPolicy) (K8sEntity, bool, error) {
    91  	containers, err := extractContainers(&entity)
    92  	if err != nil {
    93  		return K8sEntity{}, false, err
    94  	}
    95  
    96  	replaced := false
    97  	for _, c := range containers {
    98  		existingRef, err := container.ParseNamed(c.Image)
    99  		if err != nil {
   100  			return K8sEntity{}, false, err
   101  		}
   102  
   103  		if selector.Matches(existingRef) {
   104  			c.Image = container.FamiliarString(injectRef)
   105  			c.ImagePullPolicy = policy
   106  			replaced = true
   107  		}
   108  	}
   109  
   110  	return entity, replaced, nil
   111  }
   112  
   113  func injectImageDigestInEnvVars(entity K8sEntity, selector container.RefSelector, injectRef reference.Named) (K8sEntity, bool, error) {
   114  	envVars, err := extractEnvVars(&entity)
   115  	if err != nil {
   116  		return K8sEntity{}, false, err
   117  	}
   118  
   119  	replaced := false
   120  	for _, envVar := range envVars {
   121  		existingRef, err := container.ParseNamed(envVar.Value)
   122  		if err != nil || existingRef == nil {
   123  			continue
   124  		}
   125  
   126  		if selector.Matches(existingRef) {
   127  			envVar.Value = container.FamiliarString(injectRef)
   128  			replaced = true
   129  		}
   130  	}
   131  
   132  	return entity, replaced, nil
   133  }
   134  
   135  func InjectCommandAndArgs(entity K8sEntity, ref reference.Named,
   136  	cmd *v1alpha1.ImageMapOverrideCommand, args *v1alpha1.ImageMapOverrideArgs) (K8sEntity, error) {
   137  	entity = entity.DeepCopy()
   138  
   139  	selector := container.NewRefSelector(ref)
   140  	e, injected, err := injectCommandInContainers(entity, selector, cmd, args)
   141  	if err != nil {
   142  		return e, err
   143  	}
   144  	if !injected {
   145  		// NOTE(maia): currently we only support injecting commands into containers (i.e. the
   146  		// k8s yaml `container` block). This means we don't support injecting commands into CRDs.
   147  		return e, fmt.Errorf("could not inject command %v into entity: %s. No container found matching ref: %s. "+
   148  			"Note: command overrides only supported on containers with images, not on CRDs",
   149  			cmd.Command, entity.Name(), container.FamiliarString(ref))
   150  	}
   151  
   152  	return e, nil
   153  }
   154  
   155  func injectCommandInContainers(entity K8sEntity, selector container.RefSelector,
   156  	cmd *v1alpha1.ImageMapOverrideCommand, args *v1alpha1.ImageMapOverrideArgs) (K8sEntity, bool, error) {
   157  	var injected bool
   158  	containers, err := extractContainers(&entity)
   159  	if err != nil {
   160  		return K8sEntity{}, injected, err
   161  	}
   162  
   163  	for _, c := range containers {
   164  		existingRef, err := container.ParseNamed(c.Image)
   165  		if err != nil {
   166  			return K8sEntity{}, injected, err
   167  		}
   168  
   169  		if selector.Matches(existingRef) {
   170  			// The override rules of entrypoint and Command and Args are surprisingly complex!
   171  			// See this github thread:
   172  			// https://github.com/tilt-dev/tilt/issues/2918
   173  			if cmd != nil {
   174  				c.Command = cmd.Command
   175  			}
   176  
   177  			if args != nil {
   178  				c.Args = args.Args
   179  			}
   180  
   181  			injected = true
   182  		}
   183  	}
   184  	return entity, injected, nil
   185  }
   186  
   187  // HasImage indicates whether the given entity is tagged with the given image.
   188  func (e K8sEntity) HasImage(image container.RefSelector, locators []ImageLocator, inEnvVars bool) (bool, error) {
   189  	var envVarImages []container.RefSelector
   190  	if inEnvVars {
   191  		envVarImages = []container.RefSelector{image}
   192  	}
   193  	images, err := e.FindImages(locators, envVarImages)
   194  	if err != nil {
   195  		return false, errors.Wrap(err, "HasImage")
   196  	}
   197  
   198  	for _, existingRef := range images {
   199  		if image.Matches(existingRef) {
   200  			return true, nil
   201  		}
   202  	}
   203  
   204  	return false, nil
   205  }
   206  
   207  func (e K8sEntity) FindImages(locators []ImageLocator, envVarImages []container.RefSelector) ([]reference.Named, error) {
   208  	var result []reference.Named
   209  
   210  	// Look for images in instances of Container
   211  	containers, err := extractContainers(&e)
   212  	if err != nil {
   213  		return nil, err
   214  	}
   215  	for _, c := range containers {
   216  		ref, err := container.ParseNamed(c.Image)
   217  		if err != nil {
   218  			return nil, errors.Wrapf(err, "parsing %s", c.Image)
   219  		}
   220  
   221  		result = append(result, ref)
   222  	}
   223  
   224  	var obj interface{}
   225  	if u, ok := e.Obj.(runtime.Unstructured); ok {
   226  		obj = u.UnstructuredContent()
   227  	} else {
   228  		obj = e.Obj
   229  	}
   230  
   231  	for _, locator := range locators {
   232  		refs, err := locator.Extract(e)
   233  		if err != nil {
   234  			return nil, err
   235  		}
   236  
   237  		result = append(result, refs...)
   238  	}
   239  
   240  	envVars, err := extractEnvVars(&obj)
   241  	if err != nil {
   242  		return nil, err
   243  	}
   244  
   245  	for _, envVar := range envVars {
   246  		existingRef, err := container.ParseNamed(envVar.Value)
   247  		if err != nil || existingRef == nil {
   248  			continue
   249  		}
   250  		for _, img := range envVarImages {
   251  			if img.Matches(existingRef) {
   252  				result = append(result, existingRef)
   253  			}
   254  		}
   255  	}
   256  
   257  	return result, nil
   258  }
   259  
   260  func PodContainsRef(pod v1.PodSpec, selector container.RefSelector) (bool, error) {
   261  	cRef, err := FindImageRefMatching(pod, selector)
   262  	if err != nil {
   263  		return false, err
   264  	}
   265  
   266  	return cRef != nil, nil
   267  }
   268  
   269  func FindImageRefMatching(pod v1.PodSpec, selector container.RefSelector) (reference.Named, error) {
   270  	for _, c := range pod.Containers {
   271  		cRef, err := container.ParseNamed(c.Image)
   272  		if err != nil {
   273  			return nil, errors.Wrap(err, "FindImageRefMatching")
   274  		}
   275  
   276  		if selector.Matches(cRef) {
   277  			return cRef, nil
   278  		}
   279  	}
   280  	return nil, nil
   281  }
   282  
   283  func FindImageNamedTaggedMatching(pod v1.PodSpec, selector container.RefSelector) (reference.NamedTagged, error) {
   284  	cRef, err := FindImageRefMatching(pod, selector)
   285  	if err != nil {
   286  		return nil, err
   287  	}
   288  
   289  	cTagged, ok := cRef.(reference.NamedTagged)
   290  	if !ok {
   291  		return nil, nil
   292  	}
   293  
   294  	return cTagged, nil
   295  }