github.com/grahambrereton-form3/tilt@v0.10.18/internal/engine/upper.go (about)

     1  package engine
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"path/filepath"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/davecgh/go-spew/spew"
    12  	"github.com/opentracing/opentracing-go"
    13  	"github.com/pkg/errors"
    14  	"github.com/windmilleng/wmclient/pkg/analytics"
    15  	v1 "k8s.io/api/core/v1"
    16  
    17  	tiltanalytics "github.com/windmilleng/tilt/internal/analytics"
    18  	"github.com/windmilleng/tilt/internal/container"
    19  	"github.com/windmilleng/tilt/internal/dockercompose"
    20  	"github.com/windmilleng/tilt/internal/engine/configs"
    21  	"github.com/windmilleng/tilt/internal/engine/k8swatch"
    22  	"github.com/windmilleng/tilt/internal/engine/runtimelog"
    23  	"github.com/windmilleng/tilt/internal/hud"
    24  	"github.com/windmilleng/tilt/internal/hud/server"
    25  	"github.com/windmilleng/tilt/internal/k8s"
    26  	"github.com/windmilleng/tilt/internal/sliceutils"
    27  	"github.com/windmilleng/tilt/internal/store"
    28  	"github.com/windmilleng/tilt/internal/token"
    29  	"github.com/windmilleng/tilt/internal/watch"
    30  	"github.com/windmilleng/tilt/pkg/logger"
    31  	"github.com/windmilleng/tilt/pkg/model"
    32  )
    33  
    34  // When we see a file change, wait this long to see if any other files have changed, and bundle all changes together.
    35  // 200ms is not the result of any kind of research or experimentation
    36  // it might end up being a significant part of deployment delay, if we get the total latency <2s
    37  // it might also be long enough that it misses some changes if the user has some operation involving a large file
    38  //   (e.g., a binary dependency in git), but that's hopefully less of a problem since we'd get it in the next build
    39  const watchBufferMinRestInMs = 200
    40  
    41  // When waiting for a `watchBufferDurationInMs`-long break in file modifications to aggregate notifications,
    42  // if we haven't seen a break by the time `watchBufferMaxTimeInMs` has passed, just send off whatever we've got
    43  const watchBufferMaxTimeInMs = 10000
    44  
    45  var watchBufferMinRestDuration = watchBufferMinRestInMs * time.Millisecond
    46  var watchBufferMaxDuration = watchBufferMaxTimeInMs * time.Millisecond
    47  
    48  // TODO(nick): maybe this should be called 'BuildEngine' or something?
    49  // Upper seems like a poor and undescriptive name.
    50  type Upper struct {
    51  	store *store.Store
    52  }
    53  
    54  type FsWatcherMaker func(paths []string, ignore watch.PathMatcher, l logger.Logger) (watch.Notify, error)
    55  type ServiceWatcherMaker func(context.Context, *store.Store) error
    56  type PodWatcherMaker func(context.Context, *store.Store) error
    57  type timerMaker func(d time.Duration) <-chan time.Time
    58  
    59  func ProvideFsWatcherMaker() FsWatcherMaker {
    60  	return func(paths []string, ignore watch.PathMatcher, l logger.Logger) (watch.Notify, error) {
    61  		return watch.NewWatcher(paths, ignore, l)
    62  	}
    63  }
    64  
    65  func ProvideTimerMaker() timerMaker {
    66  	return func(t time.Duration) <-chan time.Time {
    67  		return time.After(t)
    68  	}
    69  }
    70  
    71  func NewUpper(ctx context.Context, st *store.Store, subs []store.Subscriber) Upper {
    72  	// There's not really a good reason to add all the subscribers
    73  	// in NewUpper(), but it's as good a place as any.
    74  	for _, sub := range subs {
    75  		st.AddSubscriber(ctx, sub)
    76  	}
    77  
    78  	return Upper{
    79  		store: st,
    80  	}
    81  }
    82  
    83  func (u Upper) Dispatch(action store.Action) {
    84  	u.store.Dispatch(action)
    85  }
    86  
    87  func (u Upper) Start(
    88  	ctx context.Context,
    89  	args []string,
    90  	b model.TiltBuild,
    91  	watch bool,
    92  	fileName string,
    93  	hudEnabled bool,
    94  	analyticsUserOpt analytics.Opt,
    95  	token token.Token,
    96  	cloudAddress string,
    97  ) error {
    98  
    99  	span, ctx := opentracing.StartSpanFromContext(ctx, "Start")
   100  	defer span.Finish()
   101  
   102  	startTime := time.Now()
   103  
   104  	absTfPath, err := filepath.Abs(fileName)
   105  	if err != nil {
   106  		return err
   107  	}
   108  
   109  	var manifestNames []model.ManifestName
   110  	for _, arg := range args {
   111  		manifestNames = append(manifestNames, model.ManifestName(arg))
   112  	}
   113  
   114  	configFiles := []string{absTfPath}
   115  
   116  	return u.Init(ctx, InitAction{
   117  		WatchFiles:       watch,
   118  		TiltfilePath:     absTfPath,
   119  		ConfigFiles:      configFiles,
   120  		InitManifests:    manifestNames,
   121  		TiltBuild:        b,
   122  		StartTime:        startTime,
   123  		AnalyticsUserOpt: analyticsUserOpt,
   124  		Token:            token,
   125  		CloudAddress:     cloudAddress,
   126  		HUDEnabled:       hudEnabled,
   127  	})
   128  }
   129  
   130  func (u Upper) Init(ctx context.Context, action InitAction) error {
   131  	u.store.Dispatch(action)
   132  	return u.store.Loop(ctx)
   133  }
   134  
   135  func upperReducerFn(ctx context.Context, state *store.EngineState, action store.Action) {
   136  	// Allow exitAction and dumpEngineStateAction even if there's a fatal error
   137  	if exitAction, isExitAction := action.(hud.ExitAction); isExitAction {
   138  		handleExitAction(state, exitAction)
   139  		return
   140  	}
   141  	if _, isDumpEngineStateAction := action.(hud.DumpEngineStateAction); isDumpEngineStateAction {
   142  		handleDumpEngineStateAction(ctx, state)
   143  		return
   144  	}
   145  
   146  	if state.FatalError != nil {
   147  		return
   148  	}
   149  
   150  	logAction, isLogAction := action.(store.LogAction)
   151  	if isLogAction {
   152  		handleLogAction(state, logAction)
   153  	}
   154  
   155  	var err error
   156  	switch action := action.(type) {
   157  	case InitAction:
   158  		err = handleInitAction(ctx, state, action)
   159  	case store.ErrorAction:
   160  		err = action.Error
   161  	case hud.ExitAction:
   162  		handleExitAction(state, action)
   163  	case targetFilesChangedAction:
   164  		handleFSEvent(ctx, state, action)
   165  	case k8swatch.PodChangeAction:
   166  		handlePodChangeAction(ctx, state, action)
   167  	case store.PodResetRestartsAction:
   168  		handlePodResetRestartsAction(state, action)
   169  	case k8swatch.ServiceChangeAction:
   170  		handleServiceEvent(ctx, state, action)
   171  	case store.K8sEventAction:
   172  		handleK8sEvent(ctx, state, action)
   173  	case runtimelog.PodLogAction:
   174  		handlePodLogAction(state, action)
   175  	case BuildLogAction:
   176  		handleBuildLogAction(state, action)
   177  	case BuildCompleteAction:
   178  		err = handleBuildCompleted(ctx, state, action)
   179  	case BuildStartedAction:
   180  		handleBuildStarted(ctx, state, action)
   181  	case configs.ConfigsReloadStartedAction:
   182  		handleConfigsReloadStarted(ctx, state, action)
   183  	case configs.ConfigsReloadedAction:
   184  		handleConfigsReloaded(ctx, state, action)
   185  	case DockerComposeEventAction:
   186  		handleDockerComposeEvent(ctx, state, action)
   187  	case runtimelog.DockerComposeLogAction:
   188  		handleDockerComposeLogAction(state, action)
   189  	case server.AppendToTriggerQueueAction:
   190  		appendToTriggerQueue(state, action.Name)
   191  	case hud.StartProfilingAction:
   192  		handleStartProfilingAction(state)
   193  	case hud.StopProfilingAction:
   194  		handleStopProfilingAction(state)
   195  	case hud.SetLogTimestampsAction:
   196  		handleLogTimestampsAction(state, action)
   197  	case configs.TiltfileLogAction:
   198  		handleTiltfileLogAction(ctx, state, action)
   199  	case hud.DumpEngineStateAction:
   200  		handleDumpEngineStateAction(ctx, state)
   201  	case LatestVersionAction:
   202  		handleLatestVersionAction(state, action)
   203  	case store.AnalyticsUserOptAction:
   204  		handleAnalyticsUserOptAction(state, action)
   205  	case store.AnalyticsNudgeSurfacedAction:
   206  		handleAnalyticsNudgeSurfacedAction(ctx, state)
   207  	case store.TiltCloudUserLookedUpAction:
   208  		handleTiltCloudUserLookedUpAction(state, action)
   209  	case store.UserStartedTiltCloudRegistrationAction:
   210  		handleUserStartedTiltCloudRegistrationAction(state)
   211  	case store.LogEvent:
   212  		// handled as a LogAction, do nothing
   213  
   214  	default:
   215  		err = fmt.Errorf("unrecognized action: %T", action)
   216  	}
   217  
   218  	if err != nil {
   219  		state.FatalError = err
   220  	}
   221  }
   222  
   223  var UpperReducer = store.Reducer(upperReducerFn)
   224  
   225  func handleBuildStarted(ctx context.Context, state *store.EngineState, action BuildStartedAction) {
   226  	mn := action.ManifestName
   227  	ms, ok := state.ManifestState(mn)
   228  	if !ok {
   229  		return
   230  	}
   231  
   232  	bs := model.BuildRecord{
   233  		Edits:     append([]string{}, action.FilesChanged...),
   234  		StartTime: action.StartTime,
   235  		Reason:    action.Reason,
   236  	}
   237  	ms.ConfigFilesThatCausedChange = []string{}
   238  	ms.CurrentBuild = bs
   239  
   240  	if ms.IsK8s() {
   241  		for _, pod := range ms.K8sRuntimeState().Pods {
   242  			pod.CurrentLog = model.Log{}
   243  			pod.UpdateStartTime = action.StartTime
   244  		}
   245  	} else if ms.IsDC() {
   246  		ms.RuntimeState = ms.DCRuntimeState().WithCurrentLog(model.Log{})
   247  	}
   248  
   249  	// Keep the crash log around until we have a rebuild
   250  	// triggered by a explicit change (i.e., not a crash rebuild)
   251  	if !action.Reason.IsCrashOnly() {
   252  		ms.CrashLog = model.Log{}
   253  	}
   254  
   255  	state.CurrentlyBuilding = mn
   256  	removeFromTriggerQueue(state, mn)
   257  }
   258  
   259  func handleBuildCompleted(ctx context.Context, engineState *store.EngineState, cb BuildCompleteAction) error {
   260  	defer func() {
   261  		engineState.CurrentlyBuilding = ""
   262  
   263  		if engineState.InitialBuildsCompleted() {
   264  			logger.Get(ctx).Debugf("[timing.py] finished initial build") // hook for timing.py
   265  		}
   266  	}()
   267  
   268  	engineState.CompletedBuildCount++
   269  	engineState.BuildControllerActionCount++
   270  	err := cb.Error
   271  
   272  	mt, ok := engineState.ManifestTargets[engineState.CurrentlyBuilding]
   273  	if !ok {
   274  		return nil
   275  	}
   276  
   277  	ms := mt.State
   278  	bs := ms.CurrentBuild
   279  	bs.Error = err
   280  	bs.FinishTime = time.Now()
   281  	bs.BuildTypes = cb.Result.BuildTypes()
   282  	ms.AddCompletedBuild(bs)
   283  
   284  	ms.CurrentBuild = model.BuildRecord{}
   285  	ms.NeedsRebuildFromCrash = false
   286  
   287  	for id, result := range cb.Result {
   288  		ms.MutableBuildStatus(id).LastResult = result
   289  	}
   290  
   291  	if err != nil {
   292  		if isFatalError(err) {
   293  			return err
   294  		} else if engineState.WatchFiles {
   295  			l := logger.Get(ctx)
   296  			p := logger.Red(l).Sprintf("Build Failed:")
   297  			l.Infof("%s %v", p, err)
   298  		} else {
   299  			return errors.Wrap(err, "Build Failed")
   300  		}
   301  	} else {
   302  		// Remove pending file changes that were consumed by this build.
   303  		for _, status := range ms.BuildStatuses {
   304  			for file, modTime := range status.PendingFileChanges {
   305  				if modTime.Before(bs.StartTime) {
   306  					delete(status.PendingFileChanges, file)
   307  				}
   308  			}
   309  		}
   310  
   311  		if !ms.PendingManifestChange.IsZero() &&
   312  			ms.PendingManifestChange.Before(bs.StartTime) {
   313  			ms.PendingManifestChange = time.Time{}
   314  		}
   315  
   316  		ms.LastSuccessfulDeployTime = time.Now()
   317  
   318  		for id, result := range cb.Result {
   319  			ms.MutableBuildStatus(id).LastSuccessfulResult = result
   320  		}
   321  
   322  		for _, pod := range ms.K8sRuntimeState().Pods {
   323  			// Reset the baseline, so that we don't show restarts
   324  			// from before any live-updates
   325  			pod.BaselineRestarts = pod.AllContainerRestarts()
   326  		}
   327  	}
   328  
   329  	// Track the container ids that have been live-updated whether the
   330  	// build succeeds or fails.
   331  	liveUpdateContainerIDs := cb.Result.LiveUpdatedContainerIDs()
   332  	if len(liveUpdateContainerIDs) == 0 {
   333  		// Assume this was an image build, and reset all the container ids
   334  		ms.LiveUpdatedContainerIDs = container.NewIDSet()
   335  	} else {
   336  		for _, cID := range liveUpdateContainerIDs {
   337  			ms.LiveUpdatedContainerIDs[cID] = true
   338  		}
   339  
   340  		bestPod := ms.MostRecentPod()
   341  		if bestPod.StartedAt.After(bs.StartTime) ||
   342  			bestPod.UpdateStartTime.Equal(bs.StartTime) {
   343  			checkForContainerCrash(ctx, engineState, mt)
   344  		}
   345  	}
   346  
   347  	manifest := mt.Manifest
   348  	deployedUIDSet := cb.Result.DeployedUIDSet()
   349  	if manifest.IsK8s() && len(deployedUIDSet) > 0 {
   350  		state := ms.GetOrCreateK8sRuntimeState()
   351  		state.DeployedUIDSet = deployedUIDSet
   352  		ms.RuntimeState = state
   353  	}
   354  
   355  	if mt.Manifest.IsDC() {
   356  		state, _ := ms.RuntimeState.(dockercompose.State)
   357  
   358  		result := cb.Result[mt.Manifest.DockerComposeTarget().ID()]
   359  		dcResult, _ := result.(store.DockerComposeBuildResult)
   360  		cid := dcResult.DockerComposeContainerID
   361  		if cid != "" {
   362  			state = state.WithContainerID(cid)
   363  		}
   364  
   365  		// If we have a container ID and no status yet, set status to Up
   366  		// (this is an expected case when we run docker-compose up while the service
   367  		// is already running, and we won't get an event to tell us so).
   368  		// If the container is crashing we will get an event subsequently.
   369  		isFirstBuild := cid != "" && state.Status == ""
   370  		if isFirstBuild {
   371  			state = state.WithStatus(dockercompose.StatusUp)
   372  		}
   373  
   374  		ms.RuntimeState = state
   375  	}
   376  
   377  	if mt.Manifest.IsLocal() {
   378  		ms.RuntimeState = store.LocalRuntimeState{HasSucceededAtLeastOnce: err == nil}
   379  	}
   380  
   381  	if engineState.WatchFiles {
   382  		logger.Get(ctx).Debugf("[timing.py] finished build from file change") // hook for timing.py
   383  	}
   384  
   385  	return nil
   386  }
   387  
   388  func appendToTriggerQueue(state *store.EngineState, mn model.ManifestName) {
   389  	_, ok := state.ManifestState(mn)
   390  	if !ok {
   391  		return
   392  	}
   393  
   394  	for _, triggerName := range state.TriggerQueue {
   395  		if mn == triggerName {
   396  			return
   397  		}
   398  	}
   399  	state.TriggerQueue = append(state.TriggerQueue, mn)
   400  }
   401  
   402  func removeFromTriggerQueue(state *store.EngineState, mn model.ManifestName) {
   403  	for i, triggerName := range state.TriggerQueue {
   404  		if triggerName == mn {
   405  			state.TriggerQueue = append(state.TriggerQueue[:i], state.TriggerQueue[i+1:]...)
   406  			break
   407  		}
   408  	}
   409  }
   410  
   411  func handleStopProfilingAction(state *store.EngineState) {
   412  	state.IsProfiling = false
   413  }
   414  
   415  func handleStartProfilingAction(state *store.EngineState) {
   416  	state.IsProfiling = true
   417  }
   418  
   419  func handleLogTimestampsAction(state *store.EngineState, action hud.SetLogTimestampsAction) {
   420  	state.LogTimestamps = action.Value
   421  }
   422  
   423  func handleFSEvent(
   424  	ctx context.Context,
   425  	state *store.EngineState,
   426  	event targetFilesChangedAction) {
   427  
   428  	if event.targetID.Type == model.TargetTypeConfigs {
   429  		for _, f := range event.files {
   430  			state.PendingConfigFileChanges[f] = event.time
   431  		}
   432  		return
   433  	}
   434  
   435  	mns := state.ManifestNamesForTargetID(event.targetID)
   436  	for _, mn := range mns {
   437  		ms, ok := state.ManifestState(mn)
   438  		if !ok {
   439  			return
   440  		}
   441  
   442  		status := ms.MutableBuildStatus(event.targetID)
   443  		for _, f := range event.files {
   444  			status.PendingFileChanges[f] = event.time
   445  		}
   446  	}
   447  }
   448  
   449  func handleConfigsReloadStarted(
   450  	ctx context.Context,
   451  	state *store.EngineState,
   452  	event configs.ConfigsReloadStartedAction,
   453  ) {
   454  	filesChanged := []string{}
   455  	for f, _ := range event.FilesChanged {
   456  		filesChanged = append(filesChanged, f)
   457  	}
   458  	status := model.BuildRecord{
   459  		StartTime: event.StartTime,
   460  		Reason:    model.BuildReasonFlagConfig,
   461  		Edits:     filesChanged,
   462  	}
   463  
   464  	state.TiltfileState.CurrentBuild = status
   465  }
   466  
   467  func handleConfigsReloaded(
   468  	ctx context.Context,
   469  	state *store.EngineState,
   470  	event configs.ConfigsReloadedAction,
   471  ) {
   472  	manifests := event.Manifests
   473  
   474  	b := state.TiltfileState.CurrentBuild
   475  
   476  	// Track the new secrets and go back to scrub them.
   477  	newSecrets := model.SecretSet{}
   478  	for k, v := range event.Secrets {
   479  		_, exists := state.Secrets[k]
   480  		if !exists {
   481  			newSecrets[k] = v
   482  		}
   483  	}
   484  
   485  	// Add all secrets, even if we failed.
   486  	state.Secrets.AddAll(event.Secrets)
   487  
   488  	// Retroactively scrub secrets
   489  	b.Log.ScrubSecretsStartingAt(newSecrets, 0)
   490  	state.Log.ScrubSecretsStartingAt(newSecrets, event.GlobalLogLineCountAtExecStart)
   491  
   492  	// if the ConfigsReloadedAction came from a unit test, there might not be a current build
   493  	if !b.Empty() {
   494  		b.FinishTime = event.FinishTime
   495  		b.Error = event.Err
   496  		b.Warnings = event.Warnings
   497  
   498  		state.TiltfileState.AddCompletedBuild(b)
   499  	}
   500  	state.TiltfileState.CurrentBuild = model.BuildRecord{}
   501  	if event.Err != nil {
   502  		// When the Tiltfile had an error, we want to differentiate between two cases:
   503  		//
   504  		// 1) You're running `tilt up` for the first time, and a local() command
   505  		// exited with status code 1.  Partial results (like enabling features)
   506  		// would be helpful.
   507  		//
   508  		// 2) You're running 'tilt up' in the happy state. You edit the Tiltfile,
   509  		// and introduce a syntax error.  You don't want partial results to wipe out
   510  		// your "good" state.
   511  
   512  		// Watch any new config files in the partial state.
   513  		state.ConfigFiles = sliceutils.AppendWithoutDupes(state.ConfigFiles, event.ConfigFiles...)
   514  
   515  		// Enable any new features in the partial state.
   516  		if len(state.Features) == 0 {
   517  			state.Features = event.Features
   518  		} else {
   519  			for feature, val := range event.Features {
   520  				if val {
   521  					state.Features[feature] = val
   522  				}
   523  			}
   524  		}
   525  		return
   526  	}
   527  
   528  	state.DockerPruneSettings = event.DockerPruneSettings
   529  
   530  	newDefOrder := make([]model.ManifestName, len(manifests))
   531  	for i, m := range manifests {
   532  		mt, ok := state.ManifestTargets[m.ManifestName()]
   533  		if !ok {
   534  			mt = store.NewManifestTarget(m)
   535  		}
   536  
   537  		newDefOrder[i] = m.ManifestName()
   538  
   539  		configFilesThatChanged := state.TiltfileState.LastBuild().Edits
   540  		old := mt.Manifest
   541  		mt.Manifest = m
   542  
   543  		if model.ChangesInvalidateBuild(old, m) {
   544  			// Manifest has changed such that the current build is invalid;
   545  			// ensure we do an image build so that we apply the changes
   546  			ms := mt.State
   547  			ms.BuildStatuses = make(map[model.TargetID]*store.BuildStatus)
   548  			ms.PendingManifestChange = time.Now()
   549  			ms.ConfigFilesThatCausedChange = configFilesThatChanged
   550  		}
   551  		state.UpsertManifestTarget(mt)
   552  	}
   553  	// TODO(dmiller) handle deleting manifests
   554  	// TODO(maia): update ConfigsManifest with new ConfigFiles/update watches
   555  	state.ManifestDefinitionOrder = newDefOrder
   556  	state.ConfigFiles = event.ConfigFiles
   557  	state.TiltIgnoreContents = event.TiltIgnoreContents
   558  
   559  	state.Features = event.Features
   560  	state.TeamName = event.TeamName
   561  
   562  	// Remove pending file changes that were consumed by this build.
   563  	for file, modTime := range state.PendingConfigFileChanges {
   564  		if modTime.Before(state.TiltfileState.LastBuild().StartTime) {
   565  			delete(state.PendingConfigFileChanges, file)
   566  		}
   567  	}
   568  }
   569  
   570  func handleBuildLogAction(state *store.EngineState, action BuildLogAction) {
   571  	manifestName := action.Source()
   572  	ms, ok := state.ManifestState(manifestName)
   573  	if !ok || state.CurrentlyBuilding != manifestName {
   574  		// This is OK. The user could have edited the manifest recently.
   575  		return
   576  	}
   577  
   578  	ms.CurrentBuild.Log = model.AppendLog(ms.CurrentBuild.Log, action, state.LogTimestamps, "", state.Secrets)
   579  }
   580  
   581  func handleLogAction(state *store.EngineState, action store.LogAction) {
   582  	manifestName := action.Source()
   583  	alreadyHasSourcePrefix := false
   584  	if _, isDCLog := action.(runtimelog.DockerComposeLogAction); isDCLog {
   585  		// DockerCompose logs are prefixed by the docker-compose engine
   586  		alreadyHasSourcePrefix = true
   587  	}
   588  
   589  	var allLogPrefix string
   590  	if manifestName != "" && !alreadyHasSourcePrefix {
   591  		allLogPrefix = sourcePrefix(manifestName)
   592  	}
   593  
   594  	state.Log = model.AppendLog(state.Log, action, state.LogTimestamps, allLogPrefix, state.Secrets)
   595  
   596  	if manifestName == "" {
   597  		return
   598  	}
   599  
   600  	ms, ok := state.ManifestState(manifestName)
   601  	if !ok {
   602  		// This is OK. The user could have edited the manifest recently.
   603  		return
   604  	}
   605  	ms.CombinedLog = model.AppendLog(ms.CombinedLog, action, state.LogTimestamps, "", state.Secrets)
   606  }
   607  
   608  func sourcePrefix(n model.ManifestName) string {
   609  	max := 12
   610  	spaces := ""
   611  	if len(n) > max {
   612  		n = n[:max-1] + "…"
   613  	} else {
   614  		spaces = strings.Repeat(" ", max-len(n))
   615  	}
   616  	return fmt.Sprintf("%s%s┊ ", n, spaces)
   617  }
   618  
   619  func handleServiceEvent(ctx context.Context, state *store.EngineState, action k8swatch.ServiceChangeAction) {
   620  	service := action.Service
   621  	ms, ok := state.ManifestState(action.ManifestName)
   622  	if !ok {
   623  		return
   624  	}
   625  
   626  	runtime := ms.GetOrCreateK8sRuntimeState()
   627  	runtime.LBs[k8s.ServiceName(service.Name)] = action.URL
   628  }
   629  
   630  func handleK8sEvent(ctx context.Context, state *store.EngineState, action store.K8sEventAction) {
   631  	evt := action.Event
   632  
   633  	if evt.Type != v1.EventTypeNormal {
   634  		handleLogAction(state, action.ToLogAction(action.ManifestName))
   635  	}
   636  }
   637  
   638  func handleDumpEngineStateAction(ctx context.Context, engineState *store.EngineState) {
   639  	f, err := ioutil.TempFile("", "tilt-engine-state-*.txt")
   640  	if err != nil {
   641  		logger.Get(ctx).Infof("error creating temp file to write engine state: %v", err)
   642  		return
   643  	}
   644  
   645  	logger.Get(ctx).Infof("dumped tilt engine state to %q", f.Name())
   646  	spew.Fdump(f, engineState)
   647  
   648  	err = f.Close()
   649  	if err != nil {
   650  		logger.Get(ctx).Infof("error closing engine state temp file: %v", err)
   651  		return
   652  	}
   653  }
   654  
   655  func handleLatestVersionAction(state *store.EngineState, action LatestVersionAction) {
   656  	state.LatestTiltBuild = action.Build
   657  }
   658  
   659  func handleInitAction(ctx context.Context, engineState *store.EngineState, action InitAction) error {
   660  	engineState.TiltBuildInfo = action.TiltBuild
   661  	engineState.TiltStartTime = action.StartTime
   662  	engineState.TiltfilePath = action.TiltfilePath
   663  	engineState.ConfigFiles = action.ConfigFiles
   664  	engineState.InitManifests = action.InitManifests
   665  	engineState.AnalyticsUserOpt = action.AnalyticsUserOpt
   666  	engineState.WatchFiles = action.WatchFiles
   667  	engineState.CloudAddress = action.CloudAddress
   668  	engineState.Token = action.Token
   669  	engineState.HUDEnabled = action.HUDEnabled
   670  
   671  	// NOTE(dmiller): this kicks off a Tiltfile build
   672  	engineState.PendingConfigFileChanges[action.TiltfilePath] = time.Now()
   673  
   674  	return nil
   675  }
   676  
   677  func handleExitAction(state *store.EngineState, action hud.ExitAction) {
   678  	if action.Err != nil {
   679  		state.FatalError = action.Err
   680  	} else {
   681  		state.UserExited = true
   682  	}
   683  }
   684  
   685  func handleDockerComposeEvent(ctx context.Context, engineState *store.EngineState, action DockerComposeEventAction) {
   686  	evt := action.Event
   687  	mn := evt.Service
   688  	ms, ok := engineState.ManifestState(model.ManifestName(mn))
   689  	if !ok {
   690  		// No corresponding manifest, nothing to do
   691  		return
   692  	}
   693  
   694  	if evt.Type != dockercompose.TypeContainer {
   695  		// We currently only support Container events.
   696  		return
   697  	}
   698  
   699  	state, _ := ms.RuntimeState.(dockercompose.State)
   700  
   701  	state = state.WithContainerID(container.ID(evt.ID))
   702  
   703  	// For now, just guess at state.
   704  	status := evt.GuessStatus()
   705  	if status != "" {
   706  		state = state.WithStatus(status)
   707  	}
   708  
   709  	if evt.IsStartupEvent() {
   710  		state = state.WithStartTime(time.Now())
   711  		state = state.WithStopping(false)
   712  		// NB: this will differ from StartTime once we support DC health checks
   713  		state = state.WithLastReadyTime(time.Now())
   714  	}
   715  
   716  	if evt.IsStopEvent() {
   717  		state = state.WithStopping(true)
   718  	}
   719  
   720  	if evt.Action == dockercompose.ActionDie && !state.IsStopping {
   721  		state = state.WithStatus(dockercompose.StatusCrash)
   722  	}
   723  
   724  	ms.RuntimeState = state
   725  }
   726  
   727  func handleDockerComposeLogAction(state *store.EngineState, action runtimelog.DockerComposeLogAction) {
   728  	manifestName := action.Source()
   729  	ms, ok := state.ManifestState(manifestName)
   730  	if !ok {
   731  		// This is OK. The user could have edited the manifest recently.
   732  		return
   733  	}
   734  
   735  	dcState, _ := ms.RuntimeState.(dockercompose.State)
   736  	ms.RuntimeState = dcState.WithCurrentLog(model.AppendLog(dcState.CurrentLog, action, state.LogTimestamps, "", state.Secrets))
   737  }
   738  
   739  func handleTiltfileLogAction(ctx context.Context, state *store.EngineState, action configs.TiltfileLogAction) {
   740  	state.TiltfileState.CurrentBuild.Log = model.AppendLog(state.TiltfileState.CurrentBuild.Log, action, state.LogTimestamps, "", state.Secrets)
   741  	state.TiltfileState.CombinedLog = model.AppendLog(state.TiltfileState.CombinedLog, action, state.LogTimestamps, "", state.Secrets)
   742  }
   743  
   744  func handleAnalyticsUserOptAction(state *store.EngineState, action store.AnalyticsUserOptAction) {
   745  	state.AnalyticsUserOpt = action.Opt
   746  }
   747  
   748  // The first time we hear that the analytics nudge was surfaced, record a metric.
   749  // We double check !state.AnalyticsNudgeSurfaced -- i.e. that the state doesn't
   750  // yet know that we've surfaced the nudge -- to ensure that we only record this
   751  // metric once (since it's an anonymous metric, we can't slice it by e.g. # unique
   752  // users, so the numbers need to be as accurate as possible).
   753  func handleAnalyticsNudgeSurfacedAction(ctx context.Context, state *store.EngineState) {
   754  	if !state.AnalyticsNudgeSurfaced {
   755  		tiltanalytics.Get(ctx).IncrIfUnopted("analytics.nudge.surfaced")
   756  		state.AnalyticsNudgeSurfaced = true
   757  	}
   758  }
   759  
   760  func handleTiltCloudUserLookedUpAction(state *store.EngineState, action store.TiltCloudUserLookedUpAction) {
   761  	if action.IsPostRegistrationLookup {
   762  		state.WaitingForTiltCloudUsernamePostRegistration = false
   763  	}
   764  	if !action.Found {
   765  		state.TokenKnownUnregistered = true
   766  		state.TiltCloudUsername = ""
   767  	} else {
   768  		state.TokenKnownUnregistered = false
   769  		state.TiltCloudUsername = action.Username
   770  	}
   771  }
   772  
   773  func handleUserStartedTiltCloudRegistrationAction(state *store.EngineState) {
   774  	state.WaitingForTiltCloudUsernamePostRegistration = true
   775  }