github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/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() 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  		// An image only needs to be pushed if it's used in-cluster.
   284  		clusterNeeds := v1alpha1.ClusterImageNeedsBase
   285  		if deployImageIDSet[iTarget.ID()] {
   286  			clusterNeeds = v1alpha1.ClusterImageNeedsPush
   287  		}
   288  
   289  		iTarget, err := iTarget.inferImageProperties(clusterNeeds, m.ClusterName())
   290  		if err != nil {
   291  			return fmt.Errorf("manifest %s: %v", m.Name, err)
   292  		}
   293  
   294  		m.ImageTargets[i] = iTarget
   295  	}
   296  	return nil
   297  }
   298  
   299  // Assemble selectors that point to other API objects created by this manifest.
   300  func (m *Manifest) InferLiveUpdateSelectors() error {
   301  	dag, err := NewTargetGraph(m.TargetSpecs())
   302  	if err != nil {
   303  		return err
   304  	}
   305  
   306  	for i, iTarget := range m.ImageTargets {
   307  		luSpec := iTarget.LiveUpdateSpec
   308  		luName := iTarget.LiveUpdateName
   309  		if luName == "" || (len(luSpec.Syncs) == 0 && len(luSpec.Execs) == 0) {
   310  			continue
   311  		}
   312  
   313  		if m.IsK8s() {
   314  			kSelector := luSpec.Selector.Kubernetes
   315  			if kSelector == nil {
   316  				kSelector = &v1alpha1.LiveUpdateKubernetesSelector{}
   317  				luSpec.Selector.Kubernetes = kSelector
   318  			}
   319  
   320  			if kSelector.ApplyName == "" {
   321  				kSelector.ApplyName = m.Name.String()
   322  			}
   323  			if kSelector.DiscoveryName == "" {
   324  				kSelector.DiscoveryName = m.Name.String()
   325  			}
   326  
   327  			// infer a selector from the ImageTarget if a container name
   328  			// selector was not specified (currently, this is always the case
   329  			// except in some k8s_custom_deploy configurations)
   330  			if kSelector.ContainerName == "" {
   331  				if iTarget.IsLiveUpdateOnly {
   332  					// use the selector (image name) as-is; Tilt isn't building
   333  					// this image, so no image name rewriting will occur
   334  					kSelector.Image = iTarget.Selector
   335  				} else {
   336  					// refer to the ImageMap so that the LU reconciler can find
   337  					// the true image name after any registry rewriting
   338  					kSelector.ImageMapName = iTarget.ImageMapName()
   339  				}
   340  			}
   341  		}
   342  
   343  		if m.IsDC() {
   344  			dcSelector := luSpec.Selector.DockerCompose
   345  			if dcSelector == nil {
   346  				dcSelector = &v1alpha1.LiveUpdateDockerComposeSelector{}
   347  				luSpec.Selector.DockerCompose = dcSelector
   348  			}
   349  
   350  			if dcSelector.Service == "" {
   351  				dcSelector.Service = m.Name.String()
   352  			}
   353  		}
   354  
   355  		luSpec.Sources = nil
   356  		err := dag.VisitTree(iTarget, func(dep TargetSpec) error {
   357  			// Relies on the idea that ImageTargets creates
   358  			// FileWatches and ImageMaps related to the ImageTarget ID.
   359  			id := dep.ID()
   360  			fw := id.String()
   361  
   362  			// LiveUpdateOnly targets do NOT have an associated image map
   363  			var imageMap string
   364  			if depImg, ok := dep.(ImageTarget); ok && !depImg.IsLiveUpdateOnly {
   365  				imageMap = id.Name.String()
   366  			}
   367  
   368  			luSpec.Sources = append(luSpec.Sources, v1alpha1.LiveUpdateSource{
   369  				FileWatch: fw,
   370  				ImageMap:  imageMap,
   371  			})
   372  			return nil
   373  		})
   374  		if err != nil {
   375  			return err
   376  		}
   377  
   378  		iTarget.LiveUpdateSpec = luSpec
   379  		m.ImageTargets[i] = iTarget
   380  	}
   381  	return nil
   382  }
   383  
   384  // Set DisableSource for any pieces of the manifest that are disable-able but not yet in the API
   385  func (m Manifest) WithDisableSource(disableSource *v1alpha1.DisableSource) Manifest {
   386  	if lt, ok := m.DeployTarget.(LocalTarget); ok {
   387  		lt.ServeCmdDisableSource = disableSource
   388  		m.DeployTarget = lt
   389  	}
   390  	return m
   391  }
   392  
   393  // ChangesInvalidateBuild checks whether the changes from old => new manifest
   394  // invalidate our build of the old one; i.e. if we're replacing `old` with `new`,
   395  // should we perform a full rebuild?
   396  func ChangesInvalidateBuild(old, new Manifest) bool {
   397  	dockerEq, k8sEq, dcEq, localEq := old.fieldGroupsEqualForBuildInvalidation(new)
   398  
   399  	return !dockerEq || !k8sEq || !dcEq || !localEq
   400  }
   401  
   402  // Compare all fields that might invalidate a build
   403  func (m1 Manifest) fieldGroupsEqualForBuildInvalidation(m2 Manifest) (dockerEq, k8sEq, dcEq, localEq bool) {
   404  	dockerEq = equalForBuildInvalidation(m1.ImageTargets, m2.ImageTargets)
   405  
   406  	dc1 := m1.DockerComposeTarget()
   407  	dc2 := m2.DockerComposeTarget()
   408  	dcEq = equalForBuildInvalidation(dc1, dc2)
   409  
   410  	k8s1 := m1.K8sTarget()
   411  	k8s2 := m2.K8sTarget()
   412  	k8sEq = equalForBuildInvalidation(k8s1, k8s2)
   413  
   414  	lt1 := m1.LocalTarget()
   415  	lt2 := m2.LocalTarget()
   416  	localEq = equalForBuildInvalidation(lt1, lt2)
   417  
   418  	return dockerEq, dcEq, k8sEq, localEq
   419  }
   420  
   421  func (m Manifest) ManifestName() ManifestName {
   422  	return m.Name
   423  }
   424  
   425  func LocalRefSelectorsForManifests(manifests []Manifest, clusters map[string]*v1alpha1.Cluster) []container.RefSelector {
   426  	var res []container.RefSelector
   427  	for _, m := range manifests {
   428  		cluster := clusters[m.ClusterName()]
   429  		for _, iTarg := range m.ImageTargets {
   430  			refs, err := iTarg.Refs(cluster)
   431  			if err != nil {
   432  				// silently ignore any invalid image references because this
   433  				// logic is only used for Docker pruning, and we can't prune
   434  				// something invalid anyway
   435  				continue
   436  			}
   437  			sel := container.NameSelector(refs.LocalRef())
   438  			res = append(res, sel)
   439  		}
   440  	}
   441  	return res
   442  }
   443  
   444  var _ TargetSpec = Manifest{}
   445  
   446  // Self-contained spec for syncing files from local to a container.
   447  //
   448  // Unlike v1alpha1.LiveUpdateSync, all fields of this object must be absolute
   449  // paths.
   450  type Sync struct {
   451  	LocalPath     string
   452  	ContainerPath string
   453  }
   454  
   455  // Self-contained spec for running in a container.
   456  //
   457  // Unlike v1alpha1.LiveUpdateExec, all fields of this object must be absolute
   458  // paths.
   459  type Run struct {
   460  	// Required. The command to run.
   461  	Cmd Cmd
   462  	// Optional. If not specified, this command runs on every change.
   463  	// If specified, we only run the Cmd if the changed file matches a trigger.
   464  	Triggers PathSet
   465  }
   466  
   467  func (r Run) WithTriggers(paths []string, baseDir string) Run {
   468  	if len(paths) > 0 {
   469  		r.Triggers = PathSet{
   470  			Paths:         paths,
   471  			BaseDirectory: baseDir,
   472  		}
   473  	} else {
   474  		r.Triggers = PathSet{}
   475  	}
   476  	return r
   477  }
   478  
   479  type PortForward struct {
   480  	// The port to connect to inside the deployed container.
   481  	// If 0, we will connect to the first containerPort.
   482  	ContainerPort int
   483  
   484  	// The port to expose on the current machine.
   485  	LocalPort int
   486  
   487  	// Optional host to bind to on the current machine (localhost by default)
   488  	Host string
   489  
   490  	// Optional name of the port forward; if given, used as text of the URL
   491  	// displayed in the web UI (e.g. <a href="localhost:8888">Debugger</a>)
   492  	Name string
   493  
   494  	// Optional path at the port forward that we link to in UIs
   495  	// (useful if e.g. nothing lives at "/" and devs will always
   496  	// want "localhost:xxxx/v1/app")
   497  	// (Private with getter/setter b/c may be nil.)
   498  	path *url.URL
   499  }
   500  
   501  func (pf PortForward) PathForAppend() string {
   502  	if pf.path == nil {
   503  		return ""
   504  	}
   505  	return strings.TrimPrefix(pf.path.String(), "/")
   506  }
   507  
   508  func (pf PortForward) WithPath(p *url.URL) PortForward {
   509  	pf.path = p
   510  	return pf
   511  }
   512  
   513  func MustPortForward(local int, container int, host string, name string, path string) PortForward {
   514  	var parsedPath *url.URL
   515  	var err error
   516  	if path != "" {
   517  		parsedPath, err = url.Parse(path)
   518  		if err != nil {
   519  			panic(err)
   520  		}
   521  	}
   522  	return PortForward{
   523  		ContainerPort: container,
   524  		LocalPort:     local,
   525  		Host:          host,
   526  		Name:          name,
   527  		path:          parsedPath,
   528  	}
   529  }
   530  
   531  // A link associated with resource; may represent a port forward, an endpoint
   532  // derived from a Service/Ingress/etc., or a URL manually associated with a
   533  // resource via the Tiltfile
   534  type Link struct {
   535  	URL *url.URL
   536  
   537  	// Optional name of the link; if given, used as text of the URL
   538  	// displayed in the web UI (e.g. <a href="localhost:8888">Debugger</a>)
   539  	Name string
   540  }
   541  
   542  func (li Link) URLString() string { return li.URL.String() }
   543  
   544  func NewLink(urlStr string, name string) (Link, error) {
   545  	u, err := url.Parse(urlStr)
   546  	if err != nil {
   547  		return Link{}, errors.Wrapf(err, "parsing URL %q", urlStr)
   548  	}
   549  	return Link{
   550  		URL:  u,
   551  		Name: name,
   552  	}, nil
   553  }
   554  
   555  func MustNewLink(urlStr string, name string) Link {
   556  	li, err := NewLink(urlStr, name)
   557  	if err != nil {
   558  		panic(err)
   559  	}
   560  	return li
   561  }
   562  
   563  // ByURL implements sort.Interface based on the URL field.
   564  type ByURL []Link
   565  
   566  func (lns ByURL) Len() int           { return len(lns) }
   567  func (lns ByURL) Less(i, j int) bool { return lns[i].URLString() < lns[j].URLString() }
   568  func (lns ByURL) Swap(i, j int)      { lns[i], lns[j] = lns[j], lns[i] }
   569  
   570  func PortForwardToLink(pf v1alpha1.Forward) Link {
   571  	host := pf.Host
   572  	if host == "" {
   573  		host = "localhost"
   574  	}
   575  	u := fmt.Sprintf("http://%s:%d/%s", host, pf.LocalPort, strings.TrimPrefix(pf.Path, "/"))
   576  
   577  	// We panic on error here because we provide the URL format ourselves,
   578  	// so if it's bad, something is very wrong.
   579  	return MustNewLink(u, pf.Name)
   580  }
   581  
   582  func LinksToURLStrings(lns []Link) []string {
   583  	res := make([]string, len(lns))
   584  	for i, ln := range lns {
   585  		res[i] = ln.URLString()
   586  	}
   587  	return res
   588  }
   589  
   590  var imageTargetAllowUnexported = cmp.AllowUnexported(ImageTarget{})
   591  var dcTargetAllowUnexported = cmp.AllowUnexported(DockerComposeTarget{})
   592  var labelRequirementAllowUnexported = cmp.AllowUnexported(labels.Requirement{})
   593  var k8sTargetAllowUnexported = cmp.AllowUnexported(K8sTarget{})
   594  var localTargetAllowUnexported = cmp.AllowUnexported(LocalTarget{})
   595  var selectorAllowUnexported = cmp.AllowUnexported(container.RefSelector{})
   596  var refSetAllowUnexported = cmp.AllowUnexported(container.RefSet{})
   597  var portForwardPathAllowUnexported = cmp.AllowUnexported(PortForward{})
   598  var ignoreCustomBuildDepsField = cmpopts.IgnoreFields(CustomBuild{}, "Deps")
   599  var ignoreLocalTargetDepsField = cmpopts.IgnoreFields(LocalTarget{}, "Deps")
   600  var ignoreDockerBuildCacheFrom = cmpopts.IgnoreFields(DockerBuild{}, "CacheFrom")
   601  var ignoreLabels = cmpopts.IgnoreFields(Manifest{}, "Labels")
   602  var ignoreDockerComposeProject = cmpopts.IgnoreFields(v1alpha1.DockerComposeServiceSpec{}, "Project")
   603  var ignoreRegistryFields = cmpopts.IgnoreFields(v1alpha1.RegistryHosting{}, "HostFromClusterNetwork", "Help")
   604  
   605  // ignoreLinks ignores user-defined links for the purpose of build invalidation
   606  //
   607  // This is done both because they don't actually invalidate the build AND because url.URL is not directly comparable
   608  // in all cases (e.g. a URL with a user@ value will result in url.URL->User being populated which has unexported fields).
   609  var ignoreLinks = cmpopts.IgnoreTypes(Link{})
   610  
   611  var dockerRefEqual = cmp.Comparer(func(a, b reference.Named) bool {
   612  	aNil := a == nil
   613  	bNil := b == nil
   614  	if aNil && bNil {
   615  		return true
   616  	}
   617  
   618  	if aNil != bNil {
   619  		return false
   620  	}
   621  
   622  	return a.String() == b.String()
   623  })
   624  
   625  // Determine whether interfaces x and y are equal, excluding fields that don't invalidate a build.
   626  func equalForBuildInvalidation(x, y interface{}) bool {
   627  	return cmp.Equal(x, y,
   628  		cmpopts.EquateEmpty(),
   629  		imageTargetAllowUnexported,
   630  		dcTargetAllowUnexported,
   631  		labelRequirementAllowUnexported,
   632  		k8sTargetAllowUnexported,
   633  		localTargetAllowUnexported,
   634  		selectorAllowUnexported,
   635  		refSetAllowUnexported,
   636  		portForwardPathAllowUnexported,
   637  		dockerRefEqual,
   638  
   639  		// deps changes don't invalidate a build, so don't compare fields used only for deps
   640  		ignoreCustomBuildDepsField,
   641  		ignoreLocalTargetDepsField,
   642  
   643  		// DockerBuild.CacheFrom doesn't invalidate a build (b/c it affects HOW we build but
   644  		// shouldn't affect the result of the build), so don't compare these fields
   645  		ignoreDockerBuildCacheFrom,
   646  
   647  		// user-added labels don't invalidate a build
   648  		ignoreLabels,
   649  
   650  		// user-added links don't invalidate a build
   651  		ignoreLinks,
   652  
   653  		// We don't want a change to the DockerCompose Project to invalidate
   654  		// all individual services. We track the service-specific YAML with
   655  		// a separate ServiceYAML field.
   656  		ignoreDockerComposeProject,
   657  
   658  		// the RegistryHosting spec includes informational fields (Help) as
   659  		// well as some unused by Tilt (HostFromClusterNetwork)
   660  		ignoreRegistryFields,
   661  	)
   662  }