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

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