github.com/tilt-dev/tilt@v0.36.0/pkg/model/manifest.go (about)

     1  package model
     2  
     3  import (
     4  	"fmt"
     5  	"net/url"
     6  	"strings"
     7  
     8  	"github.com/distribution/reference"
     9  	"github.com/google/go-cmp/cmp"
    10  	"github.com/google/go-cmp/cmp/cmpopts"
    11  	"github.com/pkg/errors"
    12  	"k8s.io/apimachinery/pkg/api/validation/path"
    13  	"k8s.io/apimachinery/pkg/labels"
    14  
    15  	"github.com/tilt-dev/tilt/internal/container"
    16  	"github.com/tilt-dev/tilt/internal/sliceutils"
    17  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    18  )
    19  
    20  // TODO(nick): We should probably get rid of ManifestName completely and just use TargetName everywhere.
    21  type ManifestName string
    22  type ManifestNameSet map[ManifestName]bool
    23  
    24  func ManifestNames(names []string) []ManifestName {
    25  	mNames := make([]ManifestName, len(names))
    26  	for i, name := range names {
    27  		mNames[i] = ManifestName(name)
    28  	}
    29  	return mNames
    30  }
    31  
    32  const MainTiltfileManifestName = ManifestName("(Tiltfile)")
    33  
    34  func (m ManifestName) String() string         { return string(m) }
    35  func (m ManifestName) TargetName() TargetName { return TargetName(m) }
    36  func (m ManifestName) TargetID() TargetID {
    37  	return TargetID{
    38  		Type: TargetTypeManifest,
    39  		Name: m.TargetName(),
    40  	}
    41  }
    42  
    43  // NOTE: If you modify Manifest, make sure to modify `equalForBuildInvalidation` appropriately
    44  type Manifest struct {
    45  	// Properties for all manifests.
    46  	Name ManifestName
    47  
    48  	// Info needed to build an image. (This struct contains details of DockerBuild, CustomBuild... etc.)
    49  	ImageTargets []ImageTarget
    50  
    51  	// Info needed to deploy. Can be k8s yaml, docker compose, etc.
    52  	DeployTarget TargetSpec
    53  
    54  	// How updates are triggered:
    55  	// - automatically, when we detect a change
    56  	// - manually, only when the user tells us to
    57  	TriggerMode TriggerMode
    58  
    59  	// The resource in this manifest will not be built until all of its dependencies have been
    60  	// ready at least once.
    61  	ResourceDependencies []ManifestName
    62  
    63  	SourceTiltfile ManifestName
    64  
    65  	Labels map[string]string
    66  }
    67  
    68  func (m Manifest) ID() TargetID {
    69  	return TargetID{
    70  		Type: TargetTypeManifest,
    71  		Name: m.Name.TargetName(),
    72  	}
    73  }
    74  
    75  func (m Manifest) DependencyIDs() []TargetID {
    76  	result := []TargetID{}
    77  	for _, iTarget := range m.ImageTargets {
    78  		result = append(result, iTarget.ID())
    79  	}
    80  	if !m.DeployTarget.ID().Empty() {
    81  		result = append(result, m.DeployTarget.ID())
    82  	}
    83  	return result
    84  }
    85  
    86  // A map from each target id to the target IDs that depend on it.
    87  func (m Manifest) ReverseDependencyIDs() map[TargetID][]TargetID {
    88  	result := make(map[TargetID][]TargetID)
    89  	for _, iTarget := range m.ImageTargets {
    90  		for _, depID := range iTarget.DependencyIDs() {
    91  			result[depID] = append(result[depID], iTarget.ID())
    92  		}
    93  	}
    94  	if !m.DeployTarget.ID().Empty() {
    95  		for _, depID := range m.DeployTarget.DependencyIDs() {
    96  			result[depID] = append(result[depID], m.DeployTarget.ID())
    97  		}
    98  	}
    99  	return result
   100  }
   101  
   102  func (m Manifest) WithImageTarget(iTarget ImageTarget) Manifest {
   103  	m.ImageTargets = []ImageTarget{iTarget}
   104  	return m
   105  }
   106  
   107  func (m Manifest) WithImageTargets(iTargets []ImageTarget) Manifest {
   108  	m.ImageTargets = append([]ImageTarget{}, iTargets...)
   109  	return m
   110  }
   111  
   112  func (m Manifest) ImageTargetAt(i int) ImageTarget {
   113  	if i < len(m.ImageTargets) {
   114  		return m.ImageTargets[i]
   115  	}
   116  	return ImageTarget{}
   117  }
   118  
   119  func (m Manifest) ImageTargetWithID(id TargetID) ImageTarget {
   120  	for _, target := range m.ImageTargets {
   121  		if target.ID() == id {
   122  			return target
   123  		}
   124  	}
   125  	return ImageTarget{}
   126  }
   127  
   128  func (m Manifest) LocalTarget() LocalTarget {
   129  	ret, _ := m.DeployTarget.(LocalTarget)
   130  	return ret
   131  }
   132  
   133  func (m Manifest) IsLocal() bool {
   134  	_, ok := m.DeployTarget.(LocalTarget)
   135  	return ok
   136  }
   137  
   138  func (m Manifest) DockerComposeTarget() DockerComposeTarget {
   139  	ret, _ := m.DeployTarget.(DockerComposeTarget)
   140  	return ret
   141  }
   142  
   143  func (m Manifest) IsDC() bool {
   144  	_, ok := m.DeployTarget.(DockerComposeTarget)
   145  	return ok
   146  }
   147  
   148  func (m Manifest) K8sTarget() K8sTarget {
   149  	ret, _ := m.DeployTarget.(K8sTarget)
   150  	return ret
   151  }
   152  
   153  func (m Manifest) IsK8s() bool {
   154  	_, ok := m.DeployTarget.(K8sTarget)
   155  	return ok
   156  }
   157  
   158  func (m Manifest) PodReadinessMode() PodReadinessMode {
   159  	if k8sTarget, ok := m.DeployTarget.(K8sTarget); ok {
   160  		return k8sTarget.PodReadinessMode
   161  	}
   162  	return PodReadinessNone
   163  }
   164  
   165  func (m Manifest) WithDeployTarget(t TargetSpec) Manifest {
   166  	switch typedTarget := t.(type) {
   167  	case K8sTarget:
   168  		typedTarget.Name = m.Name.TargetName()
   169  		t = typedTarget
   170  	case DockerComposeTarget:
   171  		typedTarget.Name = m.Name.TargetName()
   172  		t = typedTarget
   173  	}
   174  	m.DeployTarget = t
   175  	return m
   176  }
   177  
   178  func (m Manifest) WithTriggerMode(mode TriggerMode) Manifest {
   179  	m.TriggerMode = mode
   180  	return m
   181  }
   182  
   183  func (m Manifest) TargetIDSet() map[TargetID]bool {
   184  	result := make(map[TargetID]bool)
   185  	specs := m.TargetSpecs()
   186  	for _, spec := range specs {
   187  		result[spec.ID()] = true
   188  	}
   189  	return result
   190  }
   191  
   192  func (m Manifest) TargetSpecs() []TargetSpec {
   193  	result := []TargetSpec{}
   194  	for _, t := range m.ImageTargets {
   195  		result = append(result, t)
   196  	}
   197  	if m.DeployTarget != nil {
   198  		result = append(result, m.DeployTarget)
   199  	}
   200  	return result
   201  }
   202  
   203  func (m Manifest) IsImageDeployed(iTarget ImageTarget) bool {
   204  	id := iTarget.ID()
   205  	for _, depID := range m.DeployTarget.DependencyIDs() {
   206  		if depID == id {
   207  			return true
   208  		}
   209  	}
   210  	return false
   211  }
   212  
   213  func (m Manifest) LocalPaths() []string {
   214  	switch di := m.DeployTarget.(type) {
   215  	case LocalTarget:
   216  		return di.Dependencies()
   217  	case ImageTarget, K8sTarget, DockerComposeTarget:
   218  		// fall through to paths for image targets, below
   219  	}
   220  	paths := []string{}
   221  	for _, iTarget := range m.ImageTargets {
   222  		paths = append(paths, iTarget.LocalPaths()...)
   223  	}
   224  	return sliceutils.DedupedAndSorted(paths)
   225  }
   226  
   227  func (m Manifest) WithLabels(labels map[string]string) Manifest {
   228  	m.Labels = make(map[string]string)
   229  	for k, v := range labels {
   230  		m.Labels[k] = v
   231  	}
   232  	return m
   233  }
   234  
   235  func (m Manifest) Validate() error {
   236  	if m.Name == "" {
   237  		return fmt.Errorf("[validate] manifest missing name: %+v", m)
   238  	}
   239  
   240  	if errs := path.ValidatePathSegmentName(m.Name.String(), false); len(errs) != 0 {
   241  		return fmt.Errorf("invalid value %q: %v", m.Name.String(), errs[0])
   242  	}
   243  
   244  	for _, iTarget := range m.ImageTargets {
   245  		err := iTarget.Validate()
   246  		if err != nil {
   247  			return err
   248  		}
   249  	}
   250  
   251  	if m.DeployTarget != nil {
   252  		err := m.DeployTarget.Validate()
   253  		if err != nil {
   254  			return err
   255  		}
   256  	}
   257  
   258  	return nil
   259  }
   260  
   261  func (m *Manifest) ClusterName() string {
   262  	if m.IsDC() {
   263  		return v1alpha1.ClusterNameDocker
   264  	}
   265  	if m.IsK8s() {
   266  		return v1alpha1.ClusterNameDefault
   267  	}
   268  	return ""
   269  }
   270  
   271  // Infer image properties for each image.
   272  func (m *Manifest) inferImageProperties(clusterImageNeeds func(TargetID) v1alpha1.ClusterImageNeeds) error {
   273  	var deployImageIDs []TargetID
   274  	if m.DeployTarget != nil {
   275  		deployImageIDs = m.DeployTarget.DependencyIDs()
   276  	}
   277  	deployImageIDSet := make(map[TargetID]bool, len(deployImageIDs))
   278  	for _, depID := range deployImageIDs {
   279  		deployImageIDSet[depID] = true
   280  	}
   281  
   282  	for i, iTarget := range m.ImageTargets {
   283  		iTarget, err := iTarget.inferImageProperties(
   284  			clusterImageNeeds(iTarget.ID()), m.ClusterName())
   285  		if err != nil {
   286  			return fmt.Errorf("manifest %s: %v", m.Name, err)
   287  		}
   288  
   289  		m.ImageTargets[i] = iTarget
   290  	}
   291  	return nil
   292  }
   293  
   294  // Assemble selectors that point to other API objects created by this manifest.
   295  func (m *Manifest) InferLiveUpdateSelectors() error {
   296  	dag, err := NewTargetGraph(m.TargetSpecs())
   297  	if err != nil {
   298  		return err
   299  	}
   300  
   301  	for i, iTarget := range m.ImageTargets {
   302  		luSpec := iTarget.LiveUpdateSpec
   303  		luName := iTarget.LiveUpdateName
   304  		if luName == "" || (len(luSpec.Syncs) == 0 && len(luSpec.Execs) == 0) {
   305  			continue
   306  		}
   307  
   308  		if m.IsK8s() {
   309  			kSelector := luSpec.Selector.Kubernetes
   310  			if kSelector == nil {
   311  				kSelector = &v1alpha1.LiveUpdateKubernetesSelector{}
   312  				luSpec.Selector.Kubernetes = kSelector
   313  			}
   314  
   315  			if kSelector.ApplyName == "" {
   316  				kSelector.ApplyName = m.Name.String()
   317  			}
   318  			if kSelector.DiscoveryName == "" {
   319  				kSelector.DiscoveryName = m.Name.String()
   320  			}
   321  
   322  			// infer a selector from the ImageTarget if a container name
   323  			// selector was not specified (currently, this is always the case
   324  			// except in some k8s_custom_deploy configurations)
   325  			if kSelector.ContainerName == "" {
   326  				if iTarget.IsLiveUpdateOnly {
   327  					// use the selector (image name) as-is; Tilt isn't building
   328  					// this image, so no image name rewriting will occur
   329  					kSelector.Image = iTarget.Selector
   330  				} else {
   331  					// refer to the ImageMap so that the LU reconciler can find
   332  					// the true image name after any registry rewriting
   333  					kSelector.ImageMapName = iTarget.ImageMapName()
   334  				}
   335  			}
   336  		}
   337  
   338  		if m.IsDC() {
   339  			dcSelector := luSpec.Selector.DockerCompose
   340  			if dcSelector == nil {
   341  				dcSelector = &v1alpha1.LiveUpdateDockerComposeSelector{}
   342  				luSpec.Selector.DockerCompose = dcSelector
   343  			}
   344  
   345  			if dcSelector.Service == "" {
   346  				dcSelector.Service = m.Name.String()
   347  			}
   348  		}
   349  
   350  		luSpec.Sources = nil
   351  		err := dag.VisitTree(iTarget, func(dep TargetSpec) error {
   352  			// Relies on the idea that ImageTargets creates
   353  			// FileWatches and ImageMaps related to the ImageTarget ID.
   354  			id := dep.ID()
   355  			fw := id.String()
   356  
   357  			// LiveUpdateOnly targets do NOT have an associated image map
   358  			var imageMap string
   359  			if depImg, ok := dep.(ImageTarget); ok && !depImg.IsLiveUpdateOnly {
   360  				imageMap = id.Name.String()
   361  			}
   362  
   363  			luSpec.Sources = append(luSpec.Sources, v1alpha1.LiveUpdateSource{
   364  				FileWatch: fw,
   365  				ImageMap:  imageMap,
   366  			})
   367  			return nil
   368  		})
   369  		if err != nil {
   370  			return err
   371  		}
   372  
   373  		iTarget.LiveUpdateSpec = luSpec
   374  		m.ImageTargets[i] = iTarget
   375  	}
   376  	return nil
   377  }
   378  
   379  // Set DisableSource for any pieces of the manifest that are disable-able but not yet in the API
   380  func (m Manifest) WithDisableSource(disableSource *v1alpha1.DisableSource) Manifest {
   381  	if lt, ok := m.DeployTarget.(LocalTarget); ok {
   382  		lt.ServeCmdDisableSource = disableSource
   383  		m.DeployTarget = lt
   384  	}
   385  	return m
   386  }
   387  
   388  // ChangesInvalidateBuild checks whether the changes from old => new manifest
   389  // invalidate our build of the old one; i.e. if we're replacing `old` with `new`,
   390  // should we perform a full rebuild?
   391  func ChangesInvalidateBuild(old, new Manifest) bool {
   392  	dockerEq, k8sEq, dcEq, localEq := old.fieldGroupsEqualForBuildInvalidation(new)
   393  
   394  	return !dockerEq || !k8sEq || !dcEq || !localEq
   395  }
   396  
   397  // Compare all fields that might invalidate a build
   398  func (m1 Manifest) fieldGroupsEqualForBuildInvalidation(m2 Manifest) (dockerEq, k8sEq, dcEq, localEq bool) {
   399  	dockerEq = equalForBuildInvalidation(m1.ImageTargets, m2.ImageTargets)
   400  
   401  	dc1 := m1.DockerComposeTarget()
   402  	dc2 := m2.DockerComposeTarget()
   403  	dcEq = equalForBuildInvalidation(dc1, dc2)
   404  
   405  	k8s1 := m1.K8sTarget()
   406  	k8s2 := m2.K8sTarget()
   407  	k8sEq = equalForBuildInvalidation(k8s1, k8s2)
   408  
   409  	lt1 := m1.LocalTarget()
   410  	lt2 := m2.LocalTarget()
   411  	localEq = equalForBuildInvalidation(lt1, lt2)
   412  
   413  	return dockerEq, dcEq, k8sEq, localEq
   414  }
   415  
   416  func (m Manifest) ManifestName() ManifestName {
   417  	return m.Name
   418  }
   419  
   420  func LocalRefSelectorsForManifests(manifests []Manifest, clusters map[string]*v1alpha1.Cluster) []container.RefSelector {
   421  	var res []container.RefSelector
   422  	for _, m := range manifests {
   423  		cluster := clusters[m.ClusterName()]
   424  		for _, iTarg := range m.ImageTargets {
   425  			refs, err := iTarg.Refs(cluster)
   426  			if err != nil {
   427  				// silently ignore any invalid image references because this
   428  				// logic is only used for Docker pruning, and we can't prune
   429  				// something invalid anyway
   430  				continue
   431  			}
   432  			sel := container.NameSelector(refs.LocalRef())
   433  			res = append(res, sel)
   434  		}
   435  	}
   436  	return res
   437  }
   438  
   439  var _ TargetSpec = Manifest{}
   440  
   441  // Self-contained spec for syncing files from local to a container.
   442  //
   443  // Unlike v1alpha1.LiveUpdateSync, all fields of this object must be absolute
   444  // paths.
   445  type Sync struct {
   446  	LocalPath     string
   447  	ContainerPath string
   448  }
   449  
   450  // Self-contained spec for running in a container.
   451  //
   452  // Unlike v1alpha1.LiveUpdateExec, all fields of this object must be absolute
   453  // paths.
   454  type Run struct {
   455  	// Required. The command to run.
   456  	Cmd Cmd
   457  	// Optional. If not specified, this command runs on every change.
   458  	// If specified, we only run the Cmd if the changed file matches a trigger.
   459  	Triggers PathSet
   460  }
   461  
   462  func (r Run) WithTriggers(paths []string, baseDir string) Run {
   463  	if len(paths) > 0 {
   464  		r.Triggers = PathSet{
   465  			Paths:         paths,
   466  			BaseDirectory: baseDir,
   467  		}
   468  	} else {
   469  		r.Triggers = PathSet{}
   470  	}
   471  	return r
   472  }
   473  
   474  type PortForward struct {
   475  	// The port to connect to inside the deployed container.
   476  	// If 0, we will connect to the first containerPort.
   477  	ContainerPort int
   478  
   479  	// The port to expose on the current machine.
   480  	LocalPort int
   481  
   482  	// Optional host to bind to on the current machine (localhost by default)
   483  	Host string
   484  
   485  	// Optional name of the port forward; if given, used as text of the URL
   486  	// displayed in the web UI (e.g. <a href="localhost:8888">Debugger</a>)
   487  	Name string
   488  
   489  	// Optional path at the port forward that we link to in UIs
   490  	// (useful if e.g. nothing lives at "/" and devs will always
   491  	// want "localhost:xxxx/v1/app")
   492  	// (Private with getter/setter b/c may be nil.)
   493  	path *url.URL
   494  }
   495  
   496  func (pf PortForward) PathForAppend() string {
   497  	if pf.path == nil {
   498  		return ""
   499  	}
   500  	return strings.TrimPrefix(pf.path.String(), "/")
   501  }
   502  
   503  func (pf PortForward) WithPath(p *url.URL) PortForward {
   504  	pf.path = p
   505  	return pf
   506  }
   507  
   508  func MustPortForward(local int, container int, host string, name string, path string) PortForward {
   509  	var parsedPath *url.URL
   510  	var err error
   511  	if path != "" {
   512  		parsedPath, err = url.Parse(path)
   513  		if err != nil {
   514  			panic(err)
   515  		}
   516  	}
   517  	return PortForward{
   518  		ContainerPort: container,
   519  		LocalPort:     local,
   520  		Host:          host,
   521  		Name:          name,
   522  		path:          parsedPath,
   523  	}
   524  }
   525  
   526  // A link associated with resource; may represent a port forward, an endpoint
   527  // derived from a Service/Ingress/etc., or a URL manually associated with a
   528  // resource via the Tiltfile
   529  type Link struct {
   530  	URL *url.URL
   531  
   532  	// Optional name of the link; if given, used as text of the URL
   533  	// displayed in the web UI (e.g. <a href="localhost:8888">Debugger</a>)
   534  	Name string
   535  }
   536  
   537  func (li Link) URLString() string { return li.URL.String() }
   538  
   539  func NewLink(urlStr string, name string) (Link, error) {
   540  	u, err := url.Parse(urlStr)
   541  	if err != nil {
   542  		return Link{}, errors.Wrapf(err, "parsing URL %q", urlStr)
   543  	}
   544  	return Link{
   545  		URL:  u,
   546  		Name: name,
   547  	}, nil
   548  }
   549  
   550  func MustNewLink(urlStr string, name string) Link {
   551  	li, err := NewLink(urlStr, name)
   552  	if err != nil {
   553  		panic(err)
   554  	}
   555  	return li
   556  }
   557  
   558  // ByURL implements sort.Interface based on the URL field.
   559  type ByURL []Link
   560  
   561  func (lns ByURL) Len() int           { return len(lns) }
   562  func (lns ByURL) Less(i, j int) bool { return lns[i].URLString() < lns[j].URLString() }
   563  func (lns ByURL) Swap(i, j int)      { lns[i], lns[j] = lns[j], lns[i] }
   564  
   565  func PortForwardToLink(pf v1alpha1.Forward) Link {
   566  	host := pf.Host
   567  	if host == "" {
   568  		host = "localhost"
   569  	}
   570  	u := fmt.Sprintf("http://%s:%d/%s", host, pf.LocalPort, strings.TrimPrefix(pf.Path, "/"))
   571  
   572  	// We panic on error here because we provide the URL format ourselves,
   573  	// so if it's bad, something is very wrong.
   574  	return MustNewLink(u, pf.Name)
   575  }
   576  
   577  func LinksToURLStrings(lns []Link) []string {
   578  	res := make([]string, len(lns))
   579  	for i, ln := range lns {
   580  		res[i] = ln.URLString()
   581  	}
   582  	return res
   583  }
   584  
   585  var imageTargetAllowUnexported = cmp.AllowUnexported(ImageTarget{})
   586  var dcTargetAllowUnexported = cmp.AllowUnexported(DockerComposeTarget{})
   587  var labelRequirementAllowUnexported = cmp.AllowUnexported(labels.Requirement{})
   588  var k8sTargetAllowUnexported = cmp.AllowUnexported(K8sTarget{})
   589  var localTargetAllowUnexported = cmp.AllowUnexported(LocalTarget{})
   590  var selectorAllowUnexported = cmp.AllowUnexported(container.RefSelector{})
   591  var refSetAllowUnexported = cmp.AllowUnexported(container.RefSet{})
   592  var portForwardPathAllowUnexported = cmp.AllowUnexported(PortForward{})
   593  var ignoreCustomBuildDepsField = cmpopts.IgnoreFields(CustomBuild{}, "Deps")
   594  var ignoreLocalTargetDepsField = cmpopts.IgnoreFields(LocalTarget{}, "Deps")
   595  var ignoreDockerBuildCacheFrom = cmpopts.IgnoreFields(DockerBuild{}, "CacheFrom")
   596  var ignoreLabels = cmpopts.IgnoreFields(Manifest{}, "Labels")
   597  var ignoreDockerComposeProject = cmpopts.IgnoreFields(v1alpha1.DockerComposeServiceSpec{}, "Project")
   598  var ignoreRegistryFields = cmpopts.IgnoreFields(v1alpha1.RegistryHosting{}, "HostFromClusterNetwork", "Help")
   599  
   600  // ignoreLinks ignores user-defined links for the purpose of build invalidation
   601  //
   602  // This is done both because they don't actually invalidate the build AND because url.URL is not directly comparable
   603  // in all cases (e.g. a URL with a user@ value will result in url.URL->User being populated which has unexported fields).
   604  var ignoreLinks = cmpopts.IgnoreTypes(Link{})
   605  
   606  var dockerRefEqual = cmp.Comparer(func(a, b reference.Named) bool {
   607  	aNil := a == nil
   608  	bNil := b == nil
   609  	if aNil && bNil {
   610  		return true
   611  	}
   612  
   613  	if aNil != bNil {
   614  		return false
   615  	}
   616  
   617  	return a.String() == b.String()
   618  })
   619  
   620  // Determine whether interfaces x and y are equal, excluding fields that don't invalidate a build.
   621  func equalForBuildInvalidation(x, y interface{}) bool {
   622  	return cmp.Equal(x, y,
   623  		cmpopts.EquateEmpty(),
   624  		imageTargetAllowUnexported,
   625  		dcTargetAllowUnexported,
   626  		labelRequirementAllowUnexported,
   627  		k8sTargetAllowUnexported,
   628  		localTargetAllowUnexported,
   629  		selectorAllowUnexported,
   630  		refSetAllowUnexported,
   631  		portForwardPathAllowUnexported,
   632  		dockerRefEqual,
   633  
   634  		// deps changes don't invalidate a build, so don't compare fields used only for deps
   635  		ignoreCustomBuildDepsField,
   636  		ignoreLocalTargetDepsField,
   637  
   638  		// DockerBuild.CacheFrom doesn't invalidate a build (b/c it affects HOW we build but
   639  		// shouldn't affect the result of the build), so don't compare these fields
   640  		ignoreDockerBuildCacheFrom,
   641  
   642  		// user-added labels don't invalidate a build
   643  		ignoreLabels,
   644  
   645  		// user-added links don't invalidate a build
   646  		ignoreLinks,
   647  
   648  		// We don't want a change to the DockerCompose Project to invalidate
   649  		// all individual services. We track the service-specific YAML with
   650  		// a separate ServiceYAML field.
   651  		ignoreDockerComposeProject,
   652  
   653  		// the RegistryHosting spec includes informational fields (Help) as
   654  		// well as some unused by Tilt (HostFromClusterNetwork)
   655  		ignoreRegistryFields,
   656  	)
   657  }
   658  
   659  // Infer image properties for each image in the manifest set.
   660  func InferImageProperties(manifests []Manifest) error {
   661  	deployImageIDSet := make(map[TargetID]bool, len(manifests))
   662  	for _, m := range manifests {
   663  		if m.DeployTarget != nil {
   664  			for _, depID := range m.DeployTarget.DependencyIDs() {
   665  				deployImageIDSet[depID] = true
   666  			}
   667  		}
   668  	}
   669  
   670  	// An image only needs to be pushed if it's used in-cluster.
   671  	// If it needs to be pushed for one manifest, it needs to be pushed for all.
   672  	// The caching system will make sure it's not pushed multiple times.
   673  	clusterImageNeeds := func(id TargetID) v1alpha1.ClusterImageNeeds {
   674  		if deployImageIDSet[id] {
   675  			return v1alpha1.ClusterImageNeedsPush
   676  		}
   677  		return v1alpha1.ClusterImageNeedsBase
   678  	}
   679  
   680  	for _, m := range manifests {
   681  		if err := m.inferImageProperties(clusterImageNeeds); err != nil {
   682  			return err
   683  		}
   684  	}
   685  	return nil
   686  }