github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/controllers/core/tiltfile/api.go (about)

     1  package tiltfile
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strconv"
     7  	"time"
     8  
     9  	"github.com/google/go-cmp/cmp"
    10  	"k8s.io/apimachinery/pkg/api/meta"
    11  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    12  	"k8s.io/apimachinery/pkg/runtime"
    13  	"k8s.io/apimachinery/pkg/types"
    14  	"k8s.io/apimachinery/pkg/util/errors"
    15  	"sigs.k8s.io/controller-runtime/pkg/cache"
    16  	ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
    17  	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    18  
    19  	"github.com/tilt-dev/tilt/internal/controllers/apicmp"
    20  	"github.com/tilt-dev/tilt/internal/controllers/apis/liveupdate"
    21  	"github.com/tilt-dev/tilt/internal/controllers/apis/uibutton"
    22  	"github.com/tilt-dev/tilt/internal/controllers/apiset"
    23  	"github.com/tilt-dev/tilt/internal/controllers/indexer"
    24  	"github.com/tilt-dev/tilt/internal/feature"
    25  	"github.com/tilt-dev/tilt/internal/store"
    26  	"github.com/tilt-dev/tilt/internal/store/sessions"
    27  	"github.com/tilt-dev/tilt/internal/tiltfile"
    28  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    29  	"github.com/tilt-dev/tilt/pkg/model"
    30  )
    31  
    32  var (
    33  	apiGVStr = v1alpha1.SchemeGroupVersion.String()
    34  	apiKind  = "Tiltfile"
    35  	apiType  = metav1.TypeMeta{Kind: apiKind, APIVersion: apiGVStr}
    36  )
    37  
    38  type disableSourceMap map[model.ManifestName]*v1alpha1.DisableSource
    39  
    40  // Update all the objects in the apiserver that are owned by the Tiltfile.
    41  //
    42  // Here we have one big API object (the Tiltfile loader) create lots of
    43  // API objects of different types. This is not a common pattern in Kubernetes-land
    44  // (where often each type will only own one or two other types). But it's the best way
    45  // to model how the Tiltfile works.
    46  //
    47  // For that reason, this code is much more generic than owned-object creation should be.
    48  //
    49  // In the future, anything that creates objects based on the Tiltfile (e.g., FileWatch specs,
    50  // LocalServer specs) should go here.
    51  func updateOwnedObjects(
    52  	ctx context.Context,
    53  	client ctrlclient.Client,
    54  	nn types.NamespacedName,
    55  	tf *v1alpha1.Tiltfile,
    56  	tlr *tiltfile.TiltfileLoadResult,
    57  	changeEnabledResources bool,
    58  	ciTimeoutFlag model.CITimeoutFlag,
    59  	mode store.EngineMode,
    60  	defaultK8sConnection *v1alpha1.KubernetesClusterConnection,
    61  ) error {
    62  	disableSources := toDisableSources(tlr)
    63  
    64  	if tlr != nil {
    65  		for i, m := range tlr.Manifests {
    66  			// Apply the registry to the image refs.
    67  			err := m.InferImageProperties()
    68  			if err != nil {
    69  				return err
    70  			}
    71  
    72  			// Assemble the LiveUpdate selectors, connecting objects together.
    73  			err = m.InferLiveUpdateSelectors()
    74  			if err != nil {
    75  				return err
    76  			}
    77  
    78  			tlr.Manifests[i] = m.WithDisableSource(disableSources[m.Name])
    79  		}
    80  	}
    81  
    82  	apiObjects := toAPIObjects(nn, tf, tlr, ciTimeoutFlag, mode, defaultK8sConnection, disableSources)
    83  
    84  	// Propagate labels and owner references from the parent tiltfile.
    85  	for _, objMap := range apiObjects {
    86  		for _, obj := range objMap {
    87  			err := controllerutil.SetControllerReference(tf, obj, client.Scheme())
    88  			if err != nil {
    89  				return err
    90  			}
    91  			propagateLabels(tf, obj)
    92  			propagateAnnotations(tf, obj)
    93  		}
    94  	}
    95  
    96  	// Retry until the cache has started.
    97  	var retryCount = 0
    98  	var existingObjects apiset.ObjectSet
    99  	var err error
   100  	for {
   101  		existingObjects, err = getExistingAPIObjects(ctx, client, nn)
   102  		if err != nil {
   103  			if _, ok := err.(*cache.ErrCacheNotStarted); ok && retryCount < 5 {
   104  				retryCount++
   105  				time.Sleep(200 * time.Millisecond)
   106  				continue
   107  			}
   108  			return err
   109  		}
   110  		break
   111  	}
   112  
   113  	if !changeEnabledResources {
   114  		// if we're not changing enabled resources, use existing values for disable configmaps
   115  		newConfigMaps := apiObjects.GetSetForType(&v1alpha1.ConfigMap{})
   116  		oldConfigMaps := existingObjects.GetSetForType(&v1alpha1.ConfigMap{})
   117  		for _, ds := range disableSources {
   118  			if old, ok := oldConfigMaps[ds.ConfigMap.Name]; ok {
   119  				newConfigMaps[ds.ConfigMap.Name] = old
   120  			}
   121  		}
   122  	}
   123  
   124  	err = updateNewObjects(ctx, client, apiObjects, existingObjects)
   125  	if err != nil {
   126  		return err
   127  	}
   128  
   129  	// If the tiltfile loader succeeded or if the tiltfile was deleted,
   130  	// garbage collect any old objects.
   131  	//
   132  	// If the tiltfile loader failed, we want to keep those objects around, in case
   133  	// the tiltfile was only partially evaluated and is missing objects.
   134  	if tlr == nil || tlr.Error == nil {
   135  		err := removeOrphanedObjects(ctx, client, apiObjects, existingObjects)
   136  		if err != nil {
   137  			return err
   138  		}
   139  	}
   140  	return nil
   141  }
   142  
   143  // Apply labels from the Tiltfile to all objects it creates.
   144  func propagateLabels(tf *v1alpha1.Tiltfile, obj apiset.Object) {
   145  	if len(tf.Spec.Labels) > 0 {
   146  		labels := obj.GetLabels()
   147  		if labels == nil {
   148  			labels = make(map[string]string)
   149  		}
   150  		for k, v := range tf.Spec.Labels {
   151  			// Labels specified during tiltfile execution take precedence over
   152  			// labels specified in the tiltfile spec.
   153  			_, exists := labels[k]
   154  			if !exists {
   155  				labels[k] = v
   156  			}
   157  		}
   158  		obj.SetLabels(labels)
   159  	}
   160  }
   161  
   162  // We don't have a great strategy right now for assigning
   163  // API object spec definitions to Manifests in the Tilt UI.
   164  //
   165  // For now, if an object doesn't have a Manifest annotation
   166  // defined, we give it the same Manifest as the parent Tiltfile.
   167  func propagateAnnotations(tf *v1alpha1.Tiltfile, obj apiset.Object) {
   168  	annos := obj.GetAnnotations()
   169  	if annos[v1alpha1.AnnotationManifest] == "" {
   170  		if annos == nil {
   171  			annos = make(map[string]string)
   172  		}
   173  		annos[v1alpha1.AnnotationManifest] = tf.Name
   174  		obj.SetAnnotations(annos)
   175  	}
   176  }
   177  
   178  var typesWithTiltfileBuiltins = []apiset.Object{
   179  	&v1alpha1.ExtensionRepo{},
   180  	&v1alpha1.Extension{},
   181  	&v1alpha1.FileWatch{},
   182  	&v1alpha1.Cmd{},
   183  	&v1alpha1.KubernetesApply{},
   184  	&v1alpha1.UIButton{},
   185  	&v1alpha1.ConfigMap{},
   186  	&v1alpha1.KubernetesDiscovery{},
   187  }
   188  
   189  var typesToReconcile = append([]apiset.Object{
   190  	&v1alpha1.ImageMap{},
   191  	&v1alpha1.CmdImage{},
   192  	&v1alpha1.DockerImage{},
   193  	&v1alpha1.UIResource{},
   194  	&v1alpha1.LiveUpdate{},
   195  	&v1alpha1.ToggleButton{},
   196  	&v1alpha1.Cluster{},
   197  	&v1alpha1.DockerComposeService{},
   198  	&v1alpha1.Session{},
   199  }, typesWithTiltfileBuiltins...)
   200  
   201  // Fetch all the existing API objects that were generated from the Tiltfile.
   202  func getExistingAPIObjects(ctx context.Context, client ctrlclient.Client, nn types.NamespacedName) (apiset.ObjectSet, error) {
   203  	result := apiset.ObjectSet{}
   204  
   205  	// TODO(nick): Parallelize this?
   206  	for _, obj := range typesToReconcile {
   207  		list := obj.NewList().(ctrlclient.ObjectList)
   208  		err := indexer.ListOwnedBy(ctx, client, list, nn, apiType)
   209  		if err != nil {
   210  			return nil, err
   211  		}
   212  
   213  		_ = meta.EachListItem(list, func(obj runtime.Object) error {
   214  			result.Add(obj.(apiset.Object))
   215  			return nil
   216  		})
   217  	}
   218  
   219  	return result, nil
   220  }
   221  
   222  // Pulls out all the API objects generated by the Tiltfile.
   223  func toAPIObjects(
   224  	nn types.NamespacedName,
   225  	tf *v1alpha1.Tiltfile,
   226  	tlr *tiltfile.TiltfileLoadResult,
   227  	ciTimeoutFlag model.CITimeoutFlag,
   228  	mode store.EngineMode,
   229  	defaultK8sConnection *v1alpha1.KubernetesClusterConnection,
   230  	disableSources disableSourceMap,
   231  ) apiset.ObjectSet {
   232  	result := apiset.ObjectSet{}
   233  
   234  	if tlr != nil {
   235  		result.AddSetForType(&v1alpha1.ImageMap{}, toImageMapObjects(tlr, disableSources))
   236  		result.AddSetForType(&v1alpha1.LiveUpdate{}, toLiveUpdateObjects(tlr))
   237  		result.AddSetForType(&v1alpha1.DockerImage{}, toDockerImageObjects(tlr, disableSources))
   238  		result.AddSetForType(&v1alpha1.CmdImage{}, toCmdImageObjects(tlr, disableSources))
   239  
   240  		for _, obj := range typesWithTiltfileBuiltins {
   241  			result.AddSetForType(obj, tlr.ObjectSet.GetSetForType(obj))
   242  		}
   243  
   244  		result.AddSetForType(&v1alpha1.KubernetesApply{}, toKubernetesApplyObjects(tlr, disableSources))
   245  		result.AddSetForType(&v1alpha1.DockerComposeService{}, toDockerComposeServiceObjects(tlr, disableSources))
   246  		result.AddSetForType(&v1alpha1.ConfigMap{}, toDisableConfigMaps(disableSources, tlr.EnabledManifests))
   247  		result.AddSetForType(&v1alpha1.Cmd{}, toCmdObjects(tlr, disableSources))
   248  		result.AddSetForType(&v1alpha1.ToggleButton{}, toToggleButtons(disableSources))
   249  		result.AddSetForType(&v1alpha1.Cluster{}, toClusterObjects(nn, tlr, defaultK8sConnection))
   250  		result.AddSetForType(&v1alpha1.UIButton{}, toCancelButtons(tlr))
   251  	}
   252  
   253  	result.AddSetForType(&v1alpha1.Session{}, toSessionObjects(nn, tf, tlr, ciTimeoutFlag, mode))
   254  	result.AddSetForType(&v1alpha1.UIResource{}, toUIResourceObjects(tf, tlr, disableSources))
   255  
   256  	watchInputs := WatchInputs{
   257  		TiltfileManifestName: model.ManifestName(nn.Name),
   258  		EngineMode:           mode,
   259  	}
   260  
   261  	if tlr != nil {
   262  		watchInputs.Manifests = tlr.Manifests
   263  		watchInputs.ConfigFiles = tlr.ConfigFiles
   264  		watchInputs.Tiltignore = tlr.Tiltignore
   265  		watchInputs.WatchSettings = tlr.WatchSettings
   266  	}
   267  
   268  	if tf != nil {
   269  		watchInputs.TiltfilePath = tf.Spec.Path
   270  	}
   271  
   272  	result.AddSetForType(&v1alpha1.FileWatch{}, ToFileWatchObjects(watchInputs, disableSources))
   273  
   274  	return result
   275  }
   276  
   277  func disableConfigMapName(manifest model.Manifest) string {
   278  	return fmt.Sprintf("%s-disable", manifest.Name)
   279  }
   280  
   281  func toDisableSources(tlr *tiltfile.TiltfileLoadResult) disableSourceMap {
   282  	result := make(disableSourceMap)
   283  	if tlr != nil {
   284  		for _, m := range tlr.Manifests {
   285  			name := disableConfigMapName(m)
   286  			ds := &v1alpha1.DisableSource{
   287  				ConfigMap: &v1alpha1.ConfigMapDisableSource{
   288  					Name: name,
   289  					Key:  "isDisabled",
   290  				},
   291  			}
   292  			result[m.Name] = ds
   293  		}
   294  	}
   295  	return result
   296  }
   297  
   298  func appendCMDS(cms []v1alpha1.ConfigMapDisableSource, newCM v1alpha1.ConfigMapDisableSource) []v1alpha1.ConfigMapDisableSource {
   299  	for _, cm := range cms {
   300  		if apicmp.DeepEqual(cm, newCM) {
   301  			return cms
   302  		}
   303  	}
   304  	return append(cms, newCM)
   305  }
   306  
   307  func mergeDisableSource(existing *v1alpha1.DisableSource, toMerge *v1alpha1.DisableSource) *v1alpha1.DisableSource {
   308  	if toMerge == nil {
   309  		return existing
   310  	}
   311  	if apicmp.DeepEqual(existing, toMerge) {
   312  		return existing
   313  	}
   314  
   315  	cms := []v1alpha1.ConfigMapDisableSource{}
   316  
   317  	if existing.ConfigMap != nil {
   318  		cms = append(cms, *existing.ConfigMap)
   319  	}
   320  	cms = append(cms, existing.EveryConfigMap...)
   321  	if toMerge.ConfigMap != nil {
   322  		cms = appendCMDS(cms, *toMerge.ConfigMap)
   323  	}
   324  	for _, newCM := range toMerge.EveryConfigMap {
   325  		cms = appendCMDS(cms, newCM)
   326  	}
   327  	return &v1alpha1.DisableSource{EveryConfigMap: cms}
   328  }
   329  
   330  func toDisableConfigMaps(disableSources disableSourceMap, enabledResources []model.ManifestName) apiset.TypedObjectSet {
   331  	enabledResourceSet := make(map[model.ManifestName]bool)
   332  	for _, mn := range enabledResources {
   333  		enabledResourceSet[mn] = true
   334  	}
   335  	result := apiset.TypedObjectSet{}
   336  	for mn, ds := range disableSources {
   337  		isDisabled := !enabledResourceSet[mn]
   338  		cm := &v1alpha1.ConfigMap{
   339  			ObjectMeta: metav1.ObjectMeta{
   340  				Name: ds.ConfigMap.Name,
   341  			},
   342  			Data: map[string]string{ds.ConfigMap.Key: strconv.FormatBool(isDisabled)},
   343  		}
   344  		result[cm.Name] = cm
   345  	}
   346  	return result
   347  }
   348  
   349  func toToggleButtons(disableSources disableSourceMap) apiset.TypedObjectSet {
   350  	result := apiset.TypedObjectSet{}
   351  	for name, ds := range disableSources {
   352  		tb := &v1alpha1.ToggleButton{
   353  			ObjectMeta: metav1.ObjectMeta{
   354  				Name: fmt.Sprintf("%s-disable", name),
   355  				Annotations: map[string]string{
   356  					v1alpha1.AnnotationButtonType: v1alpha1.ButtonTypeDisableToggle,
   357  				},
   358  			},
   359  			Spec: v1alpha1.ToggleButtonSpec{
   360  				Location: v1alpha1.UIComponentLocation{
   361  					ComponentID:   string(name),
   362  					ComponentType: v1alpha1.ComponentTypeResource,
   363  				},
   364  				On: v1alpha1.ToggleButtonStateSpec{
   365  					Text: "Enable Resource",
   366  				},
   367  				Off: v1alpha1.ToggleButtonStateSpec{
   368  					Text:                 "Disable Resource",
   369  					RequiresConfirmation: true,
   370  				},
   371  				StateSource: v1alpha1.StateSource{
   372  					ConfigMap: &v1alpha1.ConfigMapStateSource{
   373  						Name:     ds.ConfigMap.Name,
   374  						Key:      ds.ConfigMap.Key,
   375  						OnValue:  "true",
   376  						OffValue: "false",
   377  					},
   378  				},
   379  			},
   380  		}
   381  		result[tb.Name] = tb
   382  	}
   383  	return result
   384  }
   385  
   386  func toCancelButtons(tlr *tiltfile.TiltfileLoadResult) apiset.TypedObjectSet {
   387  	result := apiset.TypedObjectSet{}
   388  	for _, m := range tlr.Manifests {
   389  		button := uibutton.StopBuildButton(m.Name.String())
   390  		result[button.Name] = button
   391  	}
   392  	return result
   393  }
   394  
   395  // Pulls out all the KubernetesApply objects generated by the Tiltfile.
   396  func toKubernetesApplyObjects(tlr *tiltfile.TiltfileLoadResult, disableSources disableSourceMap) apiset.TypedObjectSet {
   397  	result := apiset.TypedObjectSet{}
   398  	for _, m := range tlr.Manifests {
   399  		if !m.IsK8s() {
   400  			continue
   401  		}
   402  
   403  		kTarget := m.K8sTarget()
   404  		name := m.Name.String()
   405  		ka := &v1alpha1.KubernetesApply{
   406  			ObjectMeta: metav1.ObjectMeta{
   407  				Name: name,
   408  				Annotations: map[string]string{
   409  					v1alpha1.AnnotationManifest:  name,
   410  					v1alpha1.AnnotationSpanID:    fmt.Sprintf("kubernetesapply:%s", name),
   411  					v1alpha1.AnnotationManagedBy: "buildcontrol",
   412  				},
   413  			},
   414  			Spec: kTarget.KubernetesApplySpec,
   415  		}
   416  		ka.Spec.DisableSource = disableSources[m.Name]
   417  		result[name] = ka
   418  	}
   419  	return result
   420  }
   421  
   422  // Pulls out all the DockerComposeService objects generated by the Tiltfile.
   423  func toDockerComposeServiceObjects(tlr *tiltfile.TiltfileLoadResult, disableSources disableSourceMap) apiset.TypedObjectSet {
   424  	result := apiset.TypedObjectSet{}
   425  	for _, m := range tlr.Manifests {
   426  		if !m.IsDC() {
   427  			continue
   428  		}
   429  
   430  		dcTarget := m.DockerComposeTarget()
   431  		name := m.Name.String()
   432  		obj := &v1alpha1.DockerComposeService{
   433  			ObjectMeta: metav1.ObjectMeta{
   434  				Name: name,
   435  				Annotations: map[string]string{
   436  					v1alpha1.AnnotationManifest:  name,
   437  					v1alpha1.AnnotationSpanID:    fmt.Sprintf("dockercompose:%s", name),
   438  					v1alpha1.AnnotationManagedBy: "buildcontrol",
   439  				},
   440  			},
   441  			Spec: dcTarget.Spec,
   442  		}
   443  		obj.Spec.DisableSource = disableSources[m.Name]
   444  		result[name] = obj
   445  	}
   446  	return result
   447  }
   448  
   449  // Pulls out all the LiveUpdate objects generated by the Tiltfile.
   450  func toLiveUpdateObjects(tlr *tiltfile.TiltfileLoadResult) apiset.TypedObjectSet {
   451  	result := apiset.TypedObjectSet{}
   452  	for _, m := range tlr.Manifests {
   453  		for _, iTarget := range m.ImageTargets {
   454  			luSpec := iTarget.LiveUpdateSpec
   455  			luName := iTarget.LiveUpdateName
   456  			if liveupdate.IsEmptySpec(luSpec) || luName == "" {
   457  				continue
   458  			}
   459  
   460  			managedBy := ""
   461  			if !iTarget.LiveUpdateReconciler {
   462  				managedBy = "buildcontrol"
   463  			}
   464  
   465  			updateMode := liveupdate.UpdateModeAuto
   466  			if !m.TriggerMode.AutoOnChange() {
   467  				updateMode = liveupdate.UpdateModeManual
   468  			}
   469  
   470  			obj := &v1alpha1.LiveUpdate{
   471  				ObjectMeta: metav1.ObjectMeta{
   472  					Name: luName,
   473  					Annotations: map[string]string{
   474  						v1alpha1.AnnotationManifest:     m.Name.String(),
   475  						v1alpha1.AnnotationSpanID:       fmt.Sprintf("liveupdate:%s", luName),
   476  						v1alpha1.AnnotationManagedBy:    managedBy,
   477  						liveupdate.AnnotationUpdateMode: updateMode,
   478  					},
   479  				},
   480  				Spec: luSpec,
   481  			}
   482  			result[luName] = obj
   483  		}
   484  	}
   485  	return result
   486  }
   487  
   488  // Pulls out all the DockerImage objects generated by the Tiltfile.
   489  func toDockerImageObjects(tlr *tiltfile.TiltfileLoadResult, disableSources disableSourceMap) apiset.TypedObjectSet {
   490  	result := apiset.TypedObjectSet{}
   491  
   492  	for _, m := range tlr.Manifests {
   493  		for _, iTarget := range m.ImageTargets {
   494  			name := iTarget.DockerImageName
   495  			if name == "" {
   496  				continue
   497  			}
   498  
   499  			// Currently, if a DockerImage is in more than one manifest,
   500  			// we will create one per manifest.
   501  			//
   502  			// In the medium-term, we should try to annotate them in a way
   503  			// that allows manifests to share images.
   504  			di := &v1alpha1.DockerImage{
   505  				ObjectMeta: metav1.ObjectMeta{
   506  					Name: name,
   507  					Annotations: map[string]string{
   508  						v1alpha1.AnnotationManifest: m.Name.String(),
   509  						v1alpha1.AnnotationSpanID:   fmt.Sprintf("dockerimage:%s", name),
   510  					},
   511  				},
   512  				Spec: iTarget.DockerBuildInfo().DockerImageSpec,
   513  			}
   514  
   515  			// TODO(nick): Add DisableSource to image builds.
   516  			//di.Spec.DisableSource = disableSources[m.Name]
   517  
   518  			result[name] = di
   519  		}
   520  	}
   521  	return result
   522  }
   523  
   524  // Pulls out all the CmdImage objects generated by the Tiltfile.
   525  func toCmdImageObjects(tlr *tiltfile.TiltfileLoadResult, disableSources disableSourceMap) apiset.TypedObjectSet {
   526  	result := apiset.TypedObjectSet{}
   527  
   528  	for _, m := range tlr.Manifests {
   529  		for _, iTarget := range m.ImageTargets {
   530  			name := iTarget.CmdImageName
   531  			if name == "" {
   532  				continue
   533  			}
   534  
   535  			// Currently, if a CmdImage is in more than one manifest,
   536  			// we will create one per manifest.
   537  			//
   538  			// In the medium-term, we should try to annotate them in a way
   539  			// that allows manifests to share images.
   540  			ci := &v1alpha1.CmdImage{
   541  				ObjectMeta: metav1.ObjectMeta{
   542  					Name: name,
   543  					Annotations: map[string]string{
   544  						v1alpha1.AnnotationManifest: m.Name.String(),
   545  						v1alpha1.AnnotationSpanID:   fmt.Sprintf("cmdimage:%s", name),
   546  					},
   547  				},
   548  				Spec: iTarget.CustomBuildInfo().CmdImageSpec,
   549  			}
   550  
   551  			// TODO(nick): Add DisableSource to image builds.
   552  			// di.Spec.DisableSource = disableSources[m.Name]
   553  
   554  			result[name] = ci
   555  		}
   556  	}
   557  	return result
   558  }
   559  
   560  // Pulls out all the ImageMap objects generated by the Tiltfile.
   561  func toImageMapObjects(tlr *tiltfile.TiltfileLoadResult, disableSources disableSourceMap) apiset.TypedObjectSet {
   562  	result := apiset.TypedObjectSet{}
   563  
   564  	for _, m := range tlr.Manifests {
   565  		for _, iTarget := range m.ImageTargets {
   566  			if iTarget.IsLiveUpdateOnly {
   567  				continue
   568  			}
   569  
   570  			name := iTarget.ImageMapName()
   571  			_, ok := result[name]
   572  			if ok {
   573  				// Some ImageTargets are shared among multiple manifests.
   574  				continue
   575  			}
   576  
   577  			im := &v1alpha1.ImageMap{
   578  				ObjectMeta: metav1.ObjectMeta{
   579  					Name: name,
   580  					Annotations: map[string]string{
   581  						v1alpha1.AnnotationManifest: m.Name.String(),
   582  						v1alpha1.AnnotationSpanID:   fmt.Sprintf("imagemap:%s", name),
   583  					},
   584  				},
   585  				Spec: iTarget.ImageMapSpec,
   586  			}
   587  			result[name] = im
   588  		}
   589  	}
   590  	return result
   591  }
   592  
   593  // Creates an object representing the tilt session and exit conditions.
   594  func toSessionObjects(nn types.NamespacedName, tf *v1alpha1.Tiltfile, tlr *tiltfile.TiltfileLoadResult, ciTimeoutFlag model.CITimeoutFlag, mode store.EngineMode) apiset.TypedObjectSet {
   595  	result := apiset.TypedObjectSet{}
   596  	if nn.Name != model.MainTiltfileManifestName.String() {
   597  		return result
   598  	}
   599  	result[sessions.DefaultSessionName] = sessions.FromTiltfile(tf, tlr, ciTimeoutFlag, mode)
   600  	return result
   601  }
   602  
   603  // Pulls out any cluster objects generated by the tiltfile.
   604  func toClusterObjects(nn types.NamespacedName, tlr *tiltfile.TiltfileLoadResult, defaultK8sConnection *v1alpha1.KubernetesClusterConnection) apiset.TypedObjectSet {
   605  	result := apiset.TypedObjectSet{}
   606  	if nn.Name != model.MainTiltfileManifestName.String() {
   607  		return result
   608  	}
   609  
   610  	var annotations map[string]string
   611  	if tlr.FeatureFlags[feature.ClusterRefresh] {
   612  		annotations = map[string]string{
   613  			"features.tilt.dev/cluster-refresh": "true",
   614  		}
   615  	}
   616  
   617  	if tlr.HasOrchestrator(model.OrchestratorK8s) {
   618  		name := v1alpha1.ClusterNameDefault
   619  		result[name] = &v1alpha1.Cluster{
   620  			ObjectMeta: metav1.ObjectMeta{
   621  				Name:        name,
   622  				Annotations: annotations,
   623  			},
   624  			Spec: v1alpha1.ClusterSpec{
   625  				Connection: &v1alpha1.ClusterConnection{
   626  					Kubernetes: defaultK8sConnection.DeepCopy(),
   627  				},
   628  				DefaultRegistry: tlr.DefaultRegistry,
   629  			},
   630  		}
   631  	}
   632  
   633  	if tlr.HasOrchestrator(model.OrchestratorDC) {
   634  		name := v1alpha1.ClusterNameDocker
   635  		result[name] = &v1alpha1.Cluster{
   636  			ObjectMeta: metav1.ObjectMeta{
   637  				Name:        name,
   638  				Annotations: annotations,
   639  			},
   640  			Spec: v1alpha1.ClusterSpec{
   641  				Connection: &v1alpha1.ClusterConnection{
   642  					Docker: &v1alpha1.DockerClusterConnection{},
   643  				},
   644  			},
   645  		}
   646  	}
   647  
   648  	return result
   649  }
   650  
   651  // Pulls out all the Cmd objects generated by the Tiltfile.
   652  func toCmdObjects(tlr *tiltfile.TiltfileLoadResult, disableSources disableSourceMap) apiset.TypedObjectSet {
   653  	result := apiset.TypedObjectSet{}
   654  
   655  	// Every local_resource's Update Cmd gets its own object.
   656  	for _, m := range tlr.Manifests {
   657  		if !m.IsLocal() {
   658  			continue
   659  		}
   660  		localTarget := m.LocalTarget()
   661  		cmdSpec := localTarget.UpdateCmdSpec
   662  		if cmdSpec == nil {
   663  			continue
   664  		}
   665  
   666  		name := localTarget.UpdateCmdName()
   667  		cmd := &v1alpha1.Cmd{
   668  			ObjectMeta: metav1.ObjectMeta{
   669  				Name: name,
   670  				Annotations: map[string]string{
   671  					v1alpha1.AnnotationManifest:  m.Name.String(),
   672  					v1alpha1.AnnotationSpanID:    fmt.Sprintf("cmd:%s", name),
   673  					v1alpha1.AnnotationManagedBy: "local_resource",
   674  				},
   675  			},
   676  			Spec: *cmdSpec,
   677  		}
   678  		cmd.Spec.DisableSource = disableSources[m.Name]
   679  		result[name] = cmd
   680  	}
   681  
   682  	// Every custom_build Cmd gets its own Cmd object.
   683  	// It would be better for the CmdImage reconciler to create these
   684  	// and make them owned by the CmdImage.
   685  	for _, m := range tlr.Manifests {
   686  		for _, iTarget := range m.ImageTargets {
   687  			name := iTarget.CmdImageName
   688  			if name == "" {
   689  				continue
   690  			}
   691  
   692  			// Currently, if a CmdImage is in more than one manifest,
   693  			// we will create one per manifest.
   694  			//
   695  			// In the medium-term, we should try to annotate them in a way
   696  			// that allows manifests to share images.
   697  			cmdimageSpec := iTarget.CustomBuildInfo().CmdImageSpec
   698  			cmd := &v1alpha1.Cmd{
   699  				ObjectMeta: metav1.ObjectMeta{
   700  					Name: iTarget.CmdImageName,
   701  					Annotations: map[string]string{
   702  						v1alpha1.AnnotationManifest:  m.Name.String(),
   703  						v1alpha1.AnnotationSpanID:    fmt.Sprintf("cmdimage:%s", name),
   704  						v1alpha1.AnnotationManagedBy: "cmd_image",
   705  					},
   706  				},
   707  				Spec: v1alpha1.CmdSpec{
   708  					Args: cmdimageSpec.Args,
   709  					Dir:  cmdimageSpec.Dir,
   710  				},
   711  			}
   712  
   713  			// TODO(nick): Add DisableSource to image builds.
   714  			// cmd.Spec.DisableSource = disableSources[m.Name]
   715  
   716  			result[name] = cmd
   717  		}
   718  	}
   719  	return result
   720  }
   721  
   722  // Pulls out all the UIResource objects generated by the Tiltfile.
   723  func toUIResourceObjects(tf *v1alpha1.Tiltfile, tlr *tiltfile.TiltfileLoadResult, disableSources disableSourceMap) apiset.TypedObjectSet {
   724  	result := apiset.TypedObjectSet{}
   725  
   726  	if tlr != nil {
   727  		for _, m := range tlr.Manifests {
   728  			name := string(m.Name)
   729  
   730  			r := &v1alpha1.UIResource{
   731  				ObjectMeta: metav1.ObjectMeta{
   732  					Name:   name,
   733  					Labels: m.Labels,
   734  					Annotations: map[string]string{
   735  						v1alpha1.AnnotationManifest: m.Name.String(),
   736  					},
   737  				},
   738  			}
   739  
   740  			ds := disableSources[m.Name]
   741  			if ds != nil {
   742  				r.Status.DisableStatus.State = v1alpha1.DisableStatePending
   743  				r.Status.DisableStatus.Sources = []v1alpha1.DisableSource{*ds}
   744  			}
   745  
   746  			result[name] = r
   747  		}
   748  	}
   749  
   750  	if tf != nil {
   751  		result[tf.Name] = &v1alpha1.UIResource{
   752  			ObjectMeta: metav1.ObjectMeta{
   753  				Name:   tf.Name,
   754  				Labels: tf.Labels,
   755  				Annotations: map[string]string{
   756  					v1alpha1.AnnotationManifest: tf.Name,
   757  				},
   758  			},
   759  		}
   760  	}
   761  
   762  	return result
   763  }
   764  
   765  func needsUpdate(old, obj apiset.Object) bool {
   766  	// Are there other fields here we should check?
   767  	specChanged := !apicmp.DeepEqual(old.GetSpec(), obj.GetSpec())
   768  	labelsChanged := !apicmp.DeepEqual(old.GetLabels(), obj.GetLabels())
   769  	annsChanged := !apicmp.DeepEqual(old.GetAnnotations(), obj.GetAnnotations())
   770  	if specChanged || labelsChanged || annsChanged {
   771  		return true
   772  	}
   773  
   774  	// if this section ends up with more type-specific checks, we should probably move this
   775  	// to be a method on apiset.Object
   776  	if cm, ok := obj.(*v1alpha1.ConfigMap); ok {
   777  		if !cmp.Equal(cm.Data, old.(*v1alpha1.ConfigMap).Data) {
   778  			return true
   779  		}
   780  	}
   781  
   782  	return false
   783  }
   784  
   785  // Reconcile the new API objects against the existing API objects.
   786  func updateNewObjects(ctx context.Context, client ctrlclient.Client, newObjects, oldObjects apiset.ObjectSet) error {
   787  	// TODO(nick): Does it make sense to parallelize the API calls?
   788  	errs := []error{}
   789  
   790  	// Upsert the new objects.
   791  	for t, s := range newObjects {
   792  		for name, obj := range s {
   793  			var old apiset.Object
   794  			oldSet, ok := oldObjects[t]
   795  			if ok {
   796  				old = oldSet[name]
   797  			}
   798  
   799  			if old == nil {
   800  				err := client.Create(ctx, obj)
   801  				if err != nil {
   802  					errs = append(errs, fmt.Errorf("create %s/%s: %v", obj.GetGroupVersionResource().Resource, obj.GetName(), err))
   803  				}
   804  				continue
   805  			}
   806  
   807  			if needsUpdate(old, obj) {
   808  				obj.SetResourceVersion(old.GetResourceVersion())
   809  				err := client.Update(ctx, obj)
   810  				if err != nil {
   811  					errs = append(errs, fmt.Errorf("update %s/%s: %v", obj.GetGroupVersionResource().Resource, obj.GetName(), err))
   812  				}
   813  				continue
   814  			}
   815  		}
   816  	}
   817  	return errors.NewAggregate(errs)
   818  }
   819  
   820  // Garbage collect API objects that are no longer loaded.
   821  func removeOrphanedObjects(ctx context.Context, client ctrlclient.Client, newObjects, oldObjects apiset.ObjectSet) error {
   822  	// Delete any objects that aren't in the new tiltfile.
   823  	errs := []error{}
   824  	for t, s := range oldObjects {
   825  		for name, obj := range s {
   826  			newSet, ok := newObjects[t]
   827  			if ok {
   828  				_, ok := newSet[name]
   829  				if ok {
   830  					continue
   831  				}
   832  			}
   833  
   834  			err := client.Delete(ctx, obj)
   835  			if err != nil {
   836  				errs = append(errs, fmt.Errorf("delete %s/%s: %v", obj.GetGroupVersionResource().Resource, obj.GetName(), err))
   837  			}
   838  		}
   839  	}
   840  	return errors.NewAggregate(errs)
   841  }