github.com/tilt-dev/tilt@v0.36.0/internal/store/engine_state.go (about)

     1  package store
     2  
     3  import (
     4  	"fmt"
     5  	"iter"
     6  	"sort"
     7  	"time"
     8  
     9  	"github.com/tilt-dev/wmclient/pkg/analytics"
    10  
    11  	tiltanalytics "github.com/tilt-dev/tilt/internal/analytics"
    12  	"github.com/tilt-dev/tilt/internal/dockercompose"
    13  	"github.com/tilt-dev/tilt/internal/k8s"
    14  	"github.com/tilt-dev/tilt/internal/store/k8sconv"
    15  	"github.com/tilt-dev/tilt/internal/timecmp"
    16  	"github.com/tilt-dev/tilt/internal/token"
    17  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    18  	"github.com/tilt-dev/tilt/pkg/model"
    19  	"github.com/tilt-dev/tilt/pkg/model/logstore"
    20  )
    21  
    22  type EngineState struct {
    23  	TiltBuildInfo model.TiltBuild
    24  	TiltStartTime time.Time
    25  
    26  	// saved so that we can render in order
    27  	ManifestDefinitionOrder []model.ManifestName
    28  
    29  	// TODO(nick): This will eventually be a general Target index.
    30  	ManifestTargets map[model.ManifestName]*ManifestTarget
    31  
    32  	// Keep a set of the current builds, so we can quickly count how many
    33  	// builds there are without looking at all builds in the list.
    34  	CurrentBuildSet map[model.ManifestName]bool
    35  
    36  	TerminalMode TerminalMode
    37  
    38  	// For synchronizing BuildController -- wait until engine records all builds started
    39  	// so far before starting another build
    40  	BuildControllerStartCount int
    41  
    42  	// How many builds have been completed (pass or fail) since starting tilt
    43  	CompletedBuildCount int
    44  
    45  	UpdateSettings model.UpdateSettings
    46  
    47  	FatalError error
    48  
    49  	// The user has indicated they want to exit
    50  	UserExited bool
    51  
    52  	// We recovered from a panic(). We need to clean up the RTY and print the error.
    53  	PanicExited error
    54  
    55  	// Normal process termination. Either Tilt completed all its work,
    56  	// or it determined that it was unable to complete the work it was assigned.
    57  	//
    58  	// Note that ExitSignal/ExitError is never triggered in normal
    59  	// 'tilt up`/dev mode. It's more for CI modes and tilt up --watch=false modes.
    60  	//
    61  	// We don't provide the ability to customize exit codes. Either the
    62  	// process exited successfully, or with an error. In the future, we might
    63  	// add the ability to put an exit code in the error.
    64  	ExitSignal bool
    65  	ExitError  error
    66  
    67  	// All logs in Tilt, stored in a structured format.
    68  	LogStore *logstore.LogStore `testdiff:"ignore"`
    69  
    70  	TriggerQueue []model.ManifestName
    71  
    72  	TiltfileDefinitionOrder []model.ManifestName
    73  	TiltfileStates          map[model.ManifestName]*ManifestState
    74  
    75  	SuggestedTiltVersion string
    76  	VersionSettings      model.VersionSettings
    77  
    78  	// Analytics Info
    79  	AnalyticsEnvOpt        analytics.Opt
    80  	AnalyticsUserOpt       analytics.Opt // changes to this field will propagate into the TiltAnalytics subscriber + we'll record them as user choice
    81  	AnalyticsTiltfileOpt   analytics.Opt // Set by the Tiltfile. Overrides the UserOpt.
    82  	AnalyticsNudgeSurfaced bool          // this flag is set the first time we show the analytics nudge to the user.
    83  
    84  	Features map[string]bool
    85  
    86  	Secrets model.SecretSet
    87  
    88  	CloudAddress string
    89  	Token        token.Token
    90  	TeamID       string
    91  
    92  	DockerPruneSettings model.DockerPruneSettings
    93  
    94  	TelemetrySettings model.TelemetrySettings
    95  
    96  	UserConfigState model.UserConfigState
    97  
    98  	// The initialization sequence is unfortunate. Currently we have:
    99  	// 1) Dispatch an InitAction
   100  	// 1) InitAction sets DesiredTiltfilePath
   101  	// 2) ConfigsController reads DesiredTiltfilePath, writes a new Tiltfile object to the APIServer
   102  	// 4) ConfigsController dispatches a TiltfileCreateAction, to copy the apiserver data into the EngineState
   103  	DesiredTiltfilePath string
   104  
   105  	// KubernetesResources by name.
   106  	// Updated to match KubernetesApply + KubernetesDiscovery
   107  	KubernetesResources map[string]*k8sconv.KubernetesResource `json:"-"`
   108  
   109  	// API-server-based data models. Stored in EngineState
   110  	// to assist in migration.
   111  	Cmds                  map[string]*Cmd                           `json:"-"`
   112  	Tiltfiles             map[string]*v1alpha1.Tiltfile             `json:"-"`
   113  	FileWatches           map[string]*v1alpha1.FileWatch            `json:"-"`
   114  	KubernetesApplys      map[string]*v1alpha1.KubernetesApply      `json:"-"`
   115  	KubernetesDiscoverys  map[string]*v1alpha1.KubernetesDiscovery  `json:"-"`
   116  	UIResources           map[string]*v1alpha1.UIResource           `json:"-"`
   117  	ConfigMaps            map[string]*v1alpha1.ConfigMap            `json:"-"`
   118  	LiveUpdates           map[string]*v1alpha1.LiveUpdate           `json:"-"`
   119  	Clusters              map[string]*v1alpha1.Cluster              `json:"-"`
   120  	UIButtons             map[string]*v1alpha1.UIButton             `json:"-"`
   121  	DockerComposeServices map[string]*v1alpha1.DockerComposeService `json:"-"`
   122  	ImageMaps             map[string]*v1alpha1.ImageMap             `json:"-"`
   123  	DockerImages          map[string]*v1alpha1.DockerImage          `json:"-"`
   124  	CmdImages             map[string]*v1alpha1.CmdImage             `json:"-"`
   125  }
   126  
   127  func (e *EngineState) MainTiltfilePath() string {
   128  	tf, ok := e.Tiltfiles[model.MainTiltfileManifestName.String()]
   129  	if !ok {
   130  		return ""
   131  	}
   132  	return tf.Spec.Path
   133  }
   134  
   135  // Merge analytics opt-in status from different sources.
   136  // The Tiltfile opt-in takes precedence over the user opt-in.
   137  func (e *EngineState) AnalyticsEffectiveOpt() analytics.Opt {
   138  	if e.AnalyticsEnvOpt != analytics.OptDefault {
   139  		return e.AnalyticsEnvOpt
   140  	}
   141  	if e.AnalyticsTiltfileOpt != analytics.OptDefault {
   142  		return e.AnalyticsTiltfileOpt
   143  	}
   144  	return e.AnalyticsUserOpt
   145  }
   146  
   147  func (e *EngineState) ManifestNamesForTargetID(id model.TargetID) []model.ManifestName {
   148  	if id.Type == model.TargetTypeConfigs {
   149  		return []model.ManifestName{model.ManifestName(id.Name)}
   150  	}
   151  
   152  	result := make([]model.ManifestName, 0)
   153  	for mn, mt := range e.ManifestTargets {
   154  		manifest := mt.Manifest
   155  		for _, iTarget := range manifest.ImageTargets {
   156  			if iTarget.ID() == id {
   157  				result = append(result, mn)
   158  			}
   159  		}
   160  		if manifest.K8sTarget().ID() == id {
   161  			result = append(result, mn)
   162  		}
   163  		if manifest.DockerComposeTarget().ID() == id {
   164  			result = append(result, mn)
   165  		}
   166  		if manifest.LocalTarget().ID() == id {
   167  			result = append(result, mn)
   168  		}
   169  	}
   170  	return result
   171  }
   172  
   173  func (e *EngineState) IsBuilding(name model.ManifestName) bool {
   174  	ms, ok := e.ManifestState(name)
   175  	if !ok {
   176  		return false
   177  	}
   178  	return ms.IsBuilding()
   179  }
   180  
   181  // Find the first build status. Only suitable for testing.
   182  func (e *EngineState) BuildStatus(id model.TargetID) *BuildStatus {
   183  	mns := e.ManifestNamesForTargetID(id)
   184  	for _, mn := range mns {
   185  		ms := e.ManifestTargets[mn].State
   186  		bs, ok := ms.BuildStatus(id)
   187  		if ok && !bs.IsEmpty() {
   188  			return bs
   189  		}
   190  	}
   191  	return &BuildStatus{}
   192  }
   193  
   194  func (e *EngineState) AvailableBuildSlots() int {
   195  	currentBuildCount := len(e.CurrentBuildSet)
   196  	if currentBuildCount >= e.UpdateSettings.MaxParallelUpdates() {
   197  		// this could happen if user decreases max build slots while
   198  		// multiple builds are in progress, no big deal
   199  		return 0
   200  	}
   201  	return e.UpdateSettings.MaxParallelUpdates() - currentBuildCount
   202  }
   203  
   204  func (e *EngineState) UpsertManifestTarget(mt *ManifestTarget) {
   205  	mn := mt.Manifest.Name
   206  	_, ok := e.ManifestTargets[mn]
   207  	if !ok {
   208  		e.ManifestDefinitionOrder = append(e.ManifestDefinitionOrder, mn)
   209  	}
   210  	e.ManifestTargets[mn] = mt
   211  }
   212  
   213  func (e *EngineState) RemoveManifestTarget(mn model.ManifestName) {
   214  	delete(e.ManifestTargets, mn)
   215  	newOrder := []model.ManifestName{}
   216  	for _, n := range e.ManifestDefinitionOrder {
   217  		if n == mn {
   218  			continue
   219  		}
   220  		newOrder = append(newOrder, n)
   221  	}
   222  	e.ManifestDefinitionOrder = newOrder
   223  }
   224  
   225  func (e EngineState) Manifest(mn model.ManifestName) (model.Manifest, bool) {
   226  	m, ok := e.ManifestTargets[mn]
   227  	if !ok {
   228  		return model.Manifest{}, ok
   229  	}
   230  	return m.Manifest, ok
   231  }
   232  
   233  func (e EngineState) ManifestState(mn model.ManifestName) (*ManifestState, bool) {
   234  	st, ok := e.TiltfileStates[mn]
   235  	if ok {
   236  		return st, ok
   237  	}
   238  
   239  	m, ok := e.ManifestTargets[mn]
   240  	if !ok {
   241  		return nil, ok
   242  	}
   243  	return m.State, ok
   244  }
   245  
   246  // Returns Manifests in a stable order
   247  func (e EngineState) Manifests() []model.Manifest {
   248  	result := make([]model.Manifest, 0, len(e.ManifestTargets))
   249  	for _, mn := range e.ManifestDefinitionOrder {
   250  		mt, ok := e.ManifestTargets[mn]
   251  		if !ok {
   252  			continue
   253  		}
   254  		result = append(result, mt.Manifest)
   255  	}
   256  	return result
   257  }
   258  
   259  // Returns ManifestStates in a stable order
   260  func (e EngineState) ManifestStates() []*ManifestState {
   261  	result := make([]*ManifestState, 0, len(e.ManifestTargets))
   262  	for _, mn := range e.ManifestDefinitionOrder {
   263  		mt, ok := e.ManifestTargets[mn]
   264  		if !ok {
   265  			continue
   266  		}
   267  		result = append(result, mt.State)
   268  	}
   269  	return result
   270  }
   271  
   272  // Returns ManifestTargets in a stable order
   273  func (e EngineState) Targets() []*ManifestTarget {
   274  	result := make([]*ManifestTarget, 0, len(e.ManifestTargets))
   275  	for _, mn := range e.ManifestDefinitionOrder {
   276  		mt, ok := e.ManifestTargets[mn]
   277  		if !ok {
   278  			continue
   279  		}
   280  		result = append(result, mt)
   281  	}
   282  	return result
   283  }
   284  
   285  // Returns TiltfileStates in a stable order.
   286  func (e EngineState) GetTiltfileStates() []*ManifestState {
   287  	result := make([]*ManifestState, 0, len(e.TiltfileStates))
   288  	for _, mn := range e.TiltfileDefinitionOrder {
   289  		mt, ok := e.TiltfileStates[mn]
   290  		if !ok {
   291  			continue
   292  		}
   293  		result = append(result, mt)
   294  	}
   295  	return result
   296  }
   297  
   298  func (e EngineState) TargetsBesides(mn model.ManifestName) []*ManifestTarget {
   299  	targets := e.Targets()
   300  	result := make([]*ManifestTarget, 0, len(targets))
   301  	for _, mt := range targets {
   302  		if mt.Manifest.Name == mn {
   303  			continue
   304  		}
   305  
   306  		result = append(result, mt)
   307  	}
   308  	return result
   309  }
   310  
   311  func (e *EngineState) ManifestInTriggerQueue(mn model.ManifestName) bool {
   312  	for _, queued := range e.TriggerQueue {
   313  		if queued == mn {
   314  			return true
   315  		}
   316  	}
   317  	return false
   318  }
   319  
   320  func (e *EngineState) AppendToTriggerQueue(mn model.ManifestName, reason model.BuildReason) {
   321  	ms, ok := e.ManifestState(mn)
   322  	if !ok {
   323  		return
   324  	}
   325  
   326  	if reason == 0 {
   327  		reason = model.BuildReasonFlagTriggerUnknown
   328  	}
   329  
   330  	ms.TriggerReason = ms.TriggerReason.With(reason)
   331  
   332  	for _, queued := range e.TriggerQueue {
   333  		if mn == queued {
   334  			return
   335  		}
   336  	}
   337  	e.TriggerQueue = append(e.TriggerQueue, mn)
   338  }
   339  
   340  func (e *EngineState) RemoveFromTriggerQueue(mn model.ManifestName) {
   341  	mState, ok := e.ManifestState(mn)
   342  	if ok {
   343  		mState.TriggerReason = model.BuildReasonNone
   344  	}
   345  
   346  	for i, triggerName := range e.TriggerQueue {
   347  		if triggerName == mn {
   348  			e.TriggerQueue = append(e.TriggerQueue[:i], e.TriggerQueue[i+1:]...)
   349  			break
   350  		}
   351  	}
   352  }
   353  
   354  func (e EngineState) IsEmpty() bool {
   355  	return len(e.ManifestTargets) == 0
   356  }
   357  
   358  func (e EngineState) LastMainTiltfileError() error {
   359  	st, ok := e.TiltfileStates[model.MainTiltfileManifestName]
   360  	if !ok {
   361  		return nil
   362  	}
   363  
   364  	return st.LastBuild().Error
   365  }
   366  
   367  func (e *EngineState) MainTiltfileState() *ManifestState {
   368  	return e.TiltfileStates[model.MainTiltfileManifestName]
   369  }
   370  
   371  func (e *EngineState) HasBuild() bool {
   372  	for _, m := range e.Manifests() {
   373  		for _, targ := range m.ImageTargets {
   374  			if targ.IsDockerBuild() || targ.IsCustomBuild() {
   375  				return true
   376  			}
   377  		}
   378  	}
   379  	return false
   380  }
   381  
   382  func (e *EngineState) InitialBuildsCompleted() bool {
   383  	if len(e.ManifestTargets) == 0 {
   384  		return false
   385  	}
   386  
   387  	for _, mt := range e.ManifestTargets {
   388  		if !mt.Manifest.TriggerMode.AutoInitial() {
   389  			continue
   390  		}
   391  
   392  		ms, _ := e.ManifestState(mt.Manifest.Name)
   393  		if ms == nil || ms.LastBuild().Empty() {
   394  			return false
   395  		}
   396  	}
   397  
   398  	return true
   399  }
   400  
   401  type BuildStatus struct {
   402  	// We keep track of a change with two fields:
   403  	//
   404  	// 1) When we saw the change.
   405  	// 2) When we consumed the change.
   406  	//
   407  	// A change is considered pending if the last seen timestamp is later than the
   408  	// last consumed timestamp.
   409  	//
   410  	// Because these two fields are one-way ratchets (they only move forward in
   411  	// time), they avoid race conditions (e.g., if a we received Seen and Consumed
   412  	// events out of order).
   413  	ConsumedChanges time.Time
   414  
   415  	FileChanges map[string]time.Time
   416  
   417  	LastResult BuildResult
   418  
   419  	// Stores the times that dependencies were marked dirty, so we can prioritize
   420  	// the oldest one first.
   421  	//
   422  	// Long-term, we want to process all dependencies as a build graph rather than
   423  	// a list of manifests. Specifically, we'll build one Target at a time.  Once
   424  	// the build completes, we'll look at all the targets that depend on it, and
   425  	// mark DependencyChanges to indicate that they need a rebuild.
   426  	//
   427  	// Short-term, we only use this for cases where two manifests share a common
   428  	// image. This only handles cross-manifest dependencies.
   429  	//
   430  	// This approach allows us to start working on the bookkeeping and
   431  	// dependency-tracking in the short-term, without having to switch over to a
   432  	// full dependency graph in one swoop.
   433  	DependencyChanges map[model.TargetID]time.Time
   434  }
   435  
   436  func newBuildStatus() *BuildStatus {
   437  	return &BuildStatus{
   438  		FileChanges:       make(map[string]time.Time),
   439  		DependencyChanges: make(map[model.TargetID]time.Time),
   440  	}
   441  }
   442  
   443  func (s *BuildStatus) ConsumeChangesBefore(startTime time.Time) {
   444  	if s.ConsumedChanges.IsZero() || s.ConsumedChanges.Before(startTime) {
   445  		s.ConsumedChanges = startTime
   446  	}
   447  
   448  	// Garbage collect changes consumed
   449  	// more than a minute ago.
   450  	for path, modTime := range s.FileChanges {
   451  		if modTime.Add(time.Minute).Before(s.ConsumedChanges) {
   452  			delete(s.FileChanges, path)
   453  		}
   454  	}
   455  
   456  	for targetID, modTime := range s.DependencyChanges {
   457  		if modTime.Add(time.Minute).Before(s.ConsumedChanges) {
   458  			delete(s.DependencyChanges, targetID)
   459  		}
   460  	}
   461  }
   462  
   463  func (s *BuildStatus) IsEmpty() bool {
   464  	return len(s.FileChanges) == 0 &&
   465  		len(s.DependencyChanges) == 0 &&
   466  		s.LastResult == nil
   467  }
   468  
   469  // Count PendingFileChanges
   470  func (s *BuildStatus) CountPendingFileChanges() int {
   471  	count := 0
   472  	for _ = range s.PendingFileChanges() {
   473  		count++
   474  	}
   475  	return count
   476  }
   477  
   478  func (s *BuildStatus) PendingFileChanges() iter.Seq2[string, time.Time] {
   479  	return func(yield func(string, time.Time) bool) {
   480  		neverConsumed := s.ConsumedChanges.IsZero()
   481  		for p, modTime := range s.FileChanges {
   482  			if neverConsumed || !timecmp.BeforeOrEqual(modTime, s.ConsumedChanges) {
   483  				if !yield(p, modTime) {
   484  					return
   485  				}
   486  			}
   487  		}
   488  	}
   489  }
   490  
   491  func (s *BuildStatus) PendingFileChangesList() []string {
   492  	var paths []string
   493  	for p, _ := range s.PendingFileChanges() {
   494  		paths = append(paths, p)
   495  	}
   496  	return paths
   497  }
   498  
   499  func (s *BuildStatus) PendingFileChangesSorted() []string {
   500  	result := s.PendingFileChangesList()
   501  	sort.Strings(result)
   502  	return result
   503  }
   504  
   505  func (s *BuildStatus) PendingDependencyChanges() iter.Seq2[model.TargetID, time.Time] {
   506  	return func(yield func(model.TargetID, time.Time) bool) {
   507  		neverConsumed := s.ConsumedChanges.IsZero()
   508  		for p, modTime := range s.DependencyChanges {
   509  			if neverConsumed || !timecmp.BeforeOrEqual(modTime, s.ConsumedChanges) {
   510  				if !yield(p, modTime) {
   511  					return
   512  				}
   513  			}
   514  		}
   515  	}
   516  }
   517  
   518  func (s *BuildStatus) HasPendingFileChanges() bool {
   519  	for _ = range s.PendingFileChanges() {
   520  		return true
   521  	}
   522  	return false
   523  }
   524  
   525  func (s *BuildStatus) HasPendingDependencyChanges() bool {
   526  	for _ = range s.PendingDependencyChanges() {
   527  		return true
   528  	}
   529  	return false
   530  }
   531  
   532  type ManifestState struct {
   533  	Name model.ManifestName
   534  
   535  	BuildStatuses map[model.TargetID]*BuildStatus
   536  	RuntimeState  RuntimeState
   537  
   538  	PendingManifestChange time.Time
   539  
   540  	// Any current builds for this manifest.
   541  	//
   542  	// There can be multiple simultaneous image builds + deploys + live updates
   543  	// associated with a manifest.
   544  	//
   545  	// In an ideal world, we'd read these builds from the API models
   546  	// rather than do separate bookkeeping for them.
   547  	CurrentBuilds map[string]model.BuildRecord
   548  
   549  	LastSuccessfulDeployTime time.Time
   550  
   551  	// The last `BuildHistoryLimit` builds. The most recent build is first in the slice.
   552  	BuildHistory []model.BuildRecord
   553  
   554  	// If this manifest was changed, which config files led to the most recent change in manifest definition
   555  	ConfigFilesThatCausedChange []string
   556  
   557  	// If the build was manually triggered, record why.
   558  	TriggerReason model.BuildReason
   559  
   560  	DisableState v1alpha1.DisableState
   561  }
   562  
   563  func NewState() *EngineState {
   564  	ret := &EngineState{}
   565  	ret.LogStore = logstore.NewLogStore()
   566  	ret.ManifestTargets = make(map[model.ManifestName]*ManifestTarget)
   567  	ret.Secrets = model.SecretSet{}
   568  	ret.DockerPruneSettings = model.DefaultDockerPruneSettings()
   569  	ret.VersionSettings = model.VersionSettings{
   570  		CheckUpdates: true,
   571  	}
   572  	ret.UpdateSettings = model.DefaultUpdateSettings()
   573  	ret.CurrentBuildSet = make(map[model.ManifestName]bool)
   574  
   575  	// For most Tiltfiles, this is created by the TiltfileUpsertAction.  But
   576  	// lots of tests assume tha main tiltfile state exists on initialization.
   577  	ret.TiltfileDefinitionOrder = []model.ManifestName{model.MainTiltfileManifestName}
   578  	ret.TiltfileStates = map[model.ManifestName]*ManifestState{
   579  		model.MainTiltfileManifestName: NewTiltfileManifestState(model.MainTiltfileManifestName),
   580  	}
   581  
   582  	if ok, _ := tiltanalytics.IsAnalyticsDisabledFromEnv(); ok {
   583  		ret.AnalyticsEnvOpt = analytics.OptOut
   584  	}
   585  
   586  	ret.Cmds = make(map[string]*Cmd)
   587  	ret.Tiltfiles = make(map[string]*v1alpha1.Tiltfile)
   588  	ret.FileWatches = make(map[string]*v1alpha1.FileWatch)
   589  	ret.KubernetesApplys = make(map[string]*v1alpha1.KubernetesApply)
   590  	ret.DockerComposeServices = make(map[string]*v1alpha1.DockerComposeService)
   591  	ret.KubernetesDiscoverys = make(map[string]*v1alpha1.KubernetesDiscovery)
   592  	ret.KubernetesResources = make(map[string]*k8sconv.KubernetesResource)
   593  	ret.UIResources = make(map[string]*v1alpha1.UIResource)
   594  	ret.ConfigMaps = make(map[string]*v1alpha1.ConfigMap)
   595  	ret.LiveUpdates = make(map[string]*v1alpha1.LiveUpdate)
   596  	ret.Clusters = make(map[string]*v1alpha1.Cluster)
   597  	ret.UIButtons = make(map[string]*v1alpha1.UIButton)
   598  	ret.ImageMaps = make(map[string]*v1alpha1.ImageMap)
   599  	ret.DockerImages = make(map[string]*v1alpha1.DockerImage)
   600  	ret.CmdImages = make(map[string]*v1alpha1.CmdImage)
   601  
   602  	return ret
   603  }
   604  
   605  func NewTiltfileManifestState(n model.ManifestName) *ManifestState {
   606  	ms := &ManifestState{
   607  		Name:          n,
   608  		DisableState:  v1alpha1.DisableStateEnabled,
   609  		CurrentBuilds: make(map[string]model.BuildRecord),
   610  	}
   611  	ms.ResetTiltfileBuildStatus(n)
   612  	return ms
   613  }
   614  
   615  func NewManifestState(m model.Manifest) *ManifestState {
   616  	mn := m.Name
   617  	ms := &ManifestState{
   618  		Name:          mn,
   619  		DisableState:  v1alpha1.DisableStatePending,
   620  		CurrentBuilds: make(map[string]model.BuildRecord),
   621  	}
   622  
   623  	ms.ResetBuildStatus(m)
   624  
   625  	if m.IsK8s() {
   626  		ms.RuntimeState = NewK8sRuntimeState(m)
   627  	} else if m.IsLocal() {
   628  		ms.RuntimeState = LocalRuntimeState{}
   629  	}
   630  
   631  	// For historical reasons, DC state is initialized differently.
   632  
   633  	return ms
   634  }
   635  
   636  func (ms *ManifestState) ResetTiltfileBuildStatus(n model.ManifestName) {
   637  	ms.BuildStatuses = make(map[model.TargetID]*BuildStatus)
   638  	ms.BuildStatuses[model.TargetID{
   639  		Type: model.TargetTypeConfigs,
   640  		Name: model.TargetName(n),
   641  	}] = newBuildStatus()
   642  }
   643  
   644  func (ms *ManifestState) ResetBuildStatus(m model.Manifest) {
   645  	ms.BuildStatuses = make(map[model.TargetID]*BuildStatus)
   646  	for _, spec := range m.TargetSpecs() {
   647  		ms.BuildStatuses[spec.ID()] = newBuildStatus()
   648  	}
   649  }
   650  
   651  func (ms *ManifestState) TargetID() model.TargetID {
   652  	return ms.Name.TargetID()
   653  }
   654  
   655  // Returns the build status.
   656  func (ms *ManifestState) BuildStatus(id model.TargetID) (*BuildStatus, bool) {
   657  	result, ok := ms.BuildStatuses[id]
   658  	return result, ok
   659  }
   660  
   661  func (ms *ManifestState) DCRuntimeState() dockercompose.State {
   662  	ret, _ := ms.RuntimeState.(dockercompose.State)
   663  	return ret
   664  }
   665  
   666  func (ms *ManifestState) IsDC() bool {
   667  	_, ok := ms.RuntimeState.(dockercompose.State)
   668  	return ok
   669  }
   670  
   671  func (ms *ManifestState) K8sRuntimeState() K8sRuntimeState {
   672  	ret, _ := ms.RuntimeState.(K8sRuntimeState)
   673  	return ret
   674  }
   675  
   676  func (ms *ManifestState) IsK8s() bool {
   677  	_, ok := ms.RuntimeState.(K8sRuntimeState)
   678  	return ok
   679  }
   680  
   681  func (ms *ManifestState) LocalRuntimeState() LocalRuntimeState {
   682  	ret, _ := ms.RuntimeState.(LocalRuntimeState)
   683  	return ret
   684  }
   685  
   686  // Return the current build that started first.
   687  func (ms *ManifestState) EarliestCurrentBuild() model.BuildRecord {
   688  	best := model.BuildRecord{}
   689  	bestKey := ""
   690  	for k, v := range ms.CurrentBuilds {
   691  		if best.StartTime.IsZero() || best.StartTime.After(v.StartTime) || (best.StartTime == v.StartTime && k < bestKey) {
   692  			best = v
   693  			bestKey = k
   694  		}
   695  	}
   696  	return best
   697  }
   698  
   699  func (ms *ManifestState) IsBuilding() bool {
   700  	return len(ms.CurrentBuilds) != 0
   701  }
   702  
   703  func (ms *ManifestState) LastBuild() model.BuildRecord {
   704  	if len(ms.BuildHistory) == 0 {
   705  		return model.BuildRecord{}
   706  	}
   707  	return ms.BuildHistory[0]
   708  }
   709  
   710  func (ms *ManifestState) AddCompletedBuild(bs model.BuildRecord) {
   711  	ms.BuildHistory = append([]model.BuildRecord{bs}, ms.BuildHistory...)
   712  	if len(ms.BuildHistory) > model.BuildHistoryLimit {
   713  		ms.BuildHistory = ms.BuildHistory[:model.BuildHistoryLimit]
   714  	}
   715  }
   716  
   717  func (ms *ManifestState) StartedFirstBuild() bool {
   718  	return ms.IsBuilding() || len(ms.BuildHistory) > 0
   719  }
   720  
   721  func (ms *ManifestState) MostRecentPod() v1alpha1.Pod {
   722  	return ms.K8sRuntimeState().MostRecentPod()
   723  }
   724  
   725  func (ms *ManifestState) PodWithID(pid k8s.PodID) (*v1alpha1.Pod, bool) {
   726  	name := string(pid)
   727  	for _, pod := range ms.K8sRuntimeState().GetPods() {
   728  		if name == pod.Name {
   729  			return &pod, true
   730  		}
   731  	}
   732  	return nil, false
   733  }
   734  
   735  func (ms *ManifestState) AddPendingFileChange(targetID model.TargetID, file string, timestamp time.Time) {
   736  	if ms.IsBuilding() {
   737  		build := ms.EarliestCurrentBuild()
   738  		if timestamp.Before(build.StartTime) {
   739  			// this file change occurred before the build started, but if the current build already knows
   740  			// about it (from another target or rapid successive changes that weren't de-duped), it can be ignored
   741  			for _, edit := range build.Edits {
   742  				if edit == file {
   743  					return
   744  				}
   745  			}
   746  		}
   747  		// NOTE(nick): BuildController uses these timestamps to determine which files
   748  		// to clear after a build. In particular, it:
   749  		//
   750  		// 1) Grabs the pending files
   751  		// 2) Runs a live update
   752  		// 3) Clears the pending files with timestamps before the live update started.
   753  		//
   754  		// Here's the race condition: suppose a file changes, but it doesn't get into
   755  		// the EngineState until after step (2). That means step (3) will clear the file
   756  		// even though it wasn't live-updated properly. Because as far as we can tell,
   757  		// the file must have been in the EngineState before the build started.
   758  		//
   759  		// Ideally, BuildController should be do more bookkeeping to keep track of
   760  		// which files it consumed from which FileWatches. But we're changing
   761  		// this architecture anyway. For now, we record the time it got into
   762  		// the EngineState, rather than the time it was originally changed.
   763  		//
   764  		// This will all go away as we move things into reconcilers,
   765  		// because reconcilers do synchronous state updates.
   766  		isReconciler := targetID.Type == model.TargetTypeConfigs
   767  		if !isReconciler {
   768  			timestamp = time.Now()
   769  		}
   770  	}
   771  
   772  	bs, ok := ms.BuildStatus(targetID)
   773  	if ok {
   774  		bs.FileChanges[file] = timestamp
   775  	}
   776  }
   777  
   778  func (ms *ManifestState) HasPendingFileChanges() bool {
   779  	for _, status := range ms.BuildStatuses {
   780  		if status.HasPendingFileChanges() {
   781  			return true
   782  		}
   783  	}
   784  	return false
   785  }
   786  
   787  func (ms *ManifestState) HasPendingDependencyChanges() bool {
   788  	for _, status := range ms.BuildStatuses {
   789  		if status.HasPendingDependencyChanges() {
   790  			return true
   791  		}
   792  	}
   793  	return false
   794  }
   795  
   796  func (mt *ManifestTarget) NextBuildReason() model.BuildReason {
   797  	state := mt.State
   798  	reason := state.TriggerReason
   799  	if mt.State.HasPendingFileChanges() {
   800  		reason = reason.With(model.BuildReasonFlagChangedFiles)
   801  	}
   802  	if mt.State.HasPendingDependencyChanges() {
   803  		reason = reason.With(model.BuildReasonFlagChangedDeps)
   804  	}
   805  	if !mt.State.PendingManifestChange.IsZero() {
   806  		reason = reason.With(model.BuildReasonFlagConfig)
   807  	}
   808  	if !mt.State.StartedFirstBuild() && mt.Manifest.TriggerMode.AutoInitial() {
   809  		reason = reason.With(model.BuildReasonFlagInit)
   810  	}
   811  	return reason
   812  }
   813  
   814  // Whether changes have been made to this Manifest's synced files
   815  // or config since the last build.
   816  //
   817  // Returns:
   818  // bool: whether changes have been made
   819  // Time: the time of the earliest change
   820  func (ms *ManifestState) HasPendingChanges() (bool, time.Time) {
   821  	return ms.HasPendingChangesBeforeOrEqual(time.Now())
   822  }
   823  
   824  // Like HasPendingChanges, but relative to a particular time.
   825  func (ms *ManifestState) HasPendingChangesBeforeOrEqual(highWaterMark time.Time) (bool, time.Time) {
   826  	ok := false
   827  	earliest := highWaterMark
   828  	t := ms.PendingManifestChange
   829  	if !t.IsZero() && timecmp.BeforeOrEqual(t, earliest) {
   830  		ok = true
   831  		earliest = t
   832  	}
   833  
   834  	for _, status := range ms.BuildStatuses {
   835  		for _, t := range status.PendingFileChanges() {
   836  			if !t.IsZero() && timecmp.BeforeOrEqual(t, earliest) {
   837  				ok = true
   838  				earliest = t
   839  			}
   840  		}
   841  
   842  		for _, t := range status.PendingDependencyChanges() {
   843  			if !t.IsZero() && timecmp.BeforeOrEqual(t, earliest) {
   844  				ok = true
   845  				earliest = t
   846  			}
   847  		}
   848  	}
   849  	if !ok {
   850  		return ok, time.Time{}
   851  	}
   852  	return ok, earliest
   853  }
   854  
   855  func (ms *ManifestState) UpdateStatus(triggerMode model.TriggerMode) v1alpha1.UpdateStatus {
   856  	currentBuild := ms.EarliestCurrentBuild()
   857  	hasPendingChanges, _ := ms.HasPendingChanges()
   858  	lastBuild := ms.LastBuild()
   859  	lastBuildError := lastBuild.Error != nil
   860  	hasPendingBuild := false
   861  	if ms.TriggerReason != 0 {
   862  		hasPendingBuild = true
   863  	} else if triggerMode.AutoOnChange() && hasPendingChanges {
   864  		hasPendingBuild = true
   865  	} else if triggerMode.AutoInitial() && currentBuild.Empty() && lastBuild.Empty() {
   866  		hasPendingBuild = true
   867  	}
   868  
   869  	if !currentBuild.Empty() {
   870  		return v1alpha1.UpdateStatusInProgress
   871  	} else if hasPendingBuild {
   872  		return v1alpha1.UpdateStatusPending
   873  	} else if lastBuildError {
   874  		return v1alpha1.UpdateStatusError
   875  	} else if !lastBuild.Empty() {
   876  		return v1alpha1.UpdateStatusOK
   877  	}
   878  	return v1alpha1.UpdateStatusNone
   879  }
   880  
   881  // Check the runtime status of the individual status fields.
   882  //
   883  // The individual status fields don't know anything about how resources are
   884  // triggered (i.e., whether they're waiting on a dependent resource to build or
   885  // a manual trigger). So we need to consider that information here.
   886  func (ms *ManifestState) RuntimeStatus(triggerMode model.TriggerMode) v1alpha1.RuntimeStatus {
   887  	runStatus := v1alpha1.RuntimeStatusUnknown
   888  	if ms.RuntimeState != nil {
   889  		runStatus = ms.RuntimeState.RuntimeStatus()
   890  	}
   891  
   892  	if runStatus == v1alpha1.RuntimeStatusPending || runStatus == v1alpha1.RuntimeStatusUnknown {
   893  		// Let's just borrow the trigger analysis logic from UpdateStatus().
   894  		updateStatus := ms.UpdateStatus(triggerMode)
   895  		if updateStatus == v1alpha1.UpdateStatusNone {
   896  			runStatus = v1alpha1.RuntimeStatusNone
   897  		} else if updateStatus == v1alpha1.UpdateStatusPending || updateStatus == v1alpha1.UpdateStatusInProgress {
   898  			runStatus = v1alpha1.RuntimeStatusPending
   899  		}
   900  	}
   901  	return runStatus
   902  }
   903  
   904  var _ model.TargetStatus = &ManifestState{}
   905  
   906  func ManifestTargetEndpoints(mt *ManifestTarget) (endpoints []model.Link) {
   907  	if mt.Manifest.IsK8s() {
   908  		k8sTarg := mt.Manifest.K8sTarget()
   909  		endpoints = append(endpoints, k8sTarg.Links...)
   910  
   911  		// If the user specified port-forwards in the Tiltfile, we
   912  		// assume that's what they want to see in the UI (so it
   913  		// takes precedence over any load balancer URLs
   914  		portForwardSpec := k8sTarg.PortForwardTemplateSpec
   915  		if portForwardSpec != nil && len(portForwardSpec.Forwards) > 0 {
   916  			for _, pf := range portForwardSpec.Forwards {
   917  				endpoints = append(endpoints, model.PortForwardToLink(pf))
   918  			}
   919  			return endpoints
   920  		}
   921  
   922  		lbEndpoints := []model.Link{}
   923  		for _, u := range mt.State.K8sRuntimeState().LBs {
   924  			if u != nil {
   925  				lbEndpoints = append(lbEndpoints, model.Link{URL: u})
   926  			}
   927  		}
   928  		// Sort so the ordering of LB endpoints is deterministic
   929  		// (otherwise it's not, because they live in a map)
   930  		sort.Sort(model.ByURL(lbEndpoints))
   931  		endpoints = append(endpoints, lbEndpoints...)
   932  	}
   933  
   934  	localResourceLinks := mt.Manifest.LocalTarget().Links
   935  	if len(localResourceLinks) > 0 {
   936  		return localResourceLinks
   937  	}
   938  
   939  	if mt.Manifest.IsDC() {
   940  		hostPorts := make(map[int32]bool)
   941  		publishedPorts := mt.Manifest.DockerComposeTarget().PublishedPorts()
   942  		inferLinks := mt.Manifest.DockerComposeTarget().InferLinks()
   943  		for _, p := range publishedPorts {
   944  			if p == 0 || hostPorts[int32(p)] {
   945  				continue
   946  			}
   947  			hostPorts[int32(p)] = true
   948  			if inferLinks {
   949  				endpoints = append(endpoints, model.MustNewLink(fmt.Sprintf("http://localhost:%d/", p), ""))
   950  			}
   951  		}
   952  
   953  		for _, binding := range mt.State.DCRuntimeState().Ports {
   954  			// Docker usually contains multiple bindings for each port - one for ipv4 (0.0.0.0)
   955  			// and one for ipv6 (::1).
   956  			p := binding.HostPort
   957  			if hostPorts[p] {
   958  				continue
   959  			}
   960  			hostPorts[p] = true
   961  			if inferLinks {
   962  				endpoints = append(endpoints, model.MustNewLink(fmt.Sprintf("http://localhost:%d/", p), ""))
   963  			}
   964  		}
   965  
   966  		endpoints = append(endpoints, mt.Manifest.DockerComposeTarget().Links...)
   967  	}
   968  
   969  	return endpoints
   970  }
   971  
   972  const MainTiltfileManifestName = model.MainTiltfileManifestName