github.com/tilt-dev/tilt@v0.36.0/internal/engine/upper.go (about)

     1  package engine
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"time"
     9  
    10  	"github.com/davecgh/go-spew/spew"
    11  
    12  	tiltanalytics "github.com/tilt-dev/tilt/internal/analytics"
    13  	"github.com/tilt-dev/tilt/internal/controllers/core/filewatch"
    14  	ctrltiltfile "github.com/tilt-dev/tilt/internal/controllers/core/tiltfile"
    15  	"github.com/tilt-dev/tilt/internal/engine/k8swatch"
    16  	"github.com/tilt-dev/tilt/internal/engine/local"
    17  	"github.com/tilt-dev/tilt/internal/hud"
    18  	"github.com/tilt-dev/tilt/internal/hud/prompt"
    19  	"github.com/tilt-dev/tilt/internal/hud/server"
    20  	"github.com/tilt-dev/tilt/internal/k8s"
    21  	"github.com/tilt-dev/tilt/internal/store"
    22  	"github.com/tilt-dev/tilt/internal/store/buildcontrols"
    23  	"github.com/tilt-dev/tilt/internal/store/clusters"
    24  	"github.com/tilt-dev/tilt/internal/store/cmdimages"
    25  	"github.com/tilt-dev/tilt/internal/store/configmaps"
    26  	"github.com/tilt-dev/tilt/internal/store/dockercomposeservices"
    27  	"github.com/tilt-dev/tilt/internal/store/dockerimages"
    28  	"github.com/tilt-dev/tilt/internal/store/filewatches"
    29  	"github.com/tilt-dev/tilt/internal/store/imagemaps"
    30  	"github.com/tilt-dev/tilt/internal/store/kubernetesapplys"
    31  	"github.com/tilt-dev/tilt/internal/store/kubernetesdiscoverys"
    32  	"github.com/tilt-dev/tilt/internal/store/liveupdates"
    33  	"github.com/tilt-dev/tilt/internal/store/sessions"
    34  	"github.com/tilt-dev/tilt/internal/store/tiltfiles"
    35  	"github.com/tilt-dev/tilt/internal/store/uibuttons"
    36  	"github.com/tilt-dev/tilt/internal/store/uiresources"
    37  	"github.com/tilt-dev/tilt/internal/token"
    38  	"github.com/tilt-dev/tilt/pkg/logger"
    39  	"github.com/tilt-dev/tilt/pkg/model"
    40  	"github.com/tilt-dev/wmclient/pkg/analytics"
    41  )
    42  
    43  // TODO(nick): maybe this should be called 'BuildEngine' or something?
    44  // Upper seems like a poor and undescriptive name.
    45  type Upper struct {
    46  	store *store.Store
    47  }
    48  
    49  type ServiceWatcherMaker func(context.Context, *store.Store) error
    50  type PodWatcherMaker func(context.Context, *store.Store) error
    51  
    52  func NewUpper(ctx context.Context, st *store.Store, subs []store.Subscriber) (Upper, error) {
    53  	// There's not really a good reason to add all the subscribers
    54  	// in NewUpper(), but it's as good a place as any.
    55  	for _, sub := range subs {
    56  		err := st.AddSubscriber(ctx, sub)
    57  		if err != nil {
    58  			return Upper{}, err
    59  		}
    60  	}
    61  
    62  	return Upper{
    63  		store: st,
    64  	}, nil
    65  }
    66  
    67  func (u Upper) Dispatch(action store.Action) {
    68  	u.store.Dispatch(action)
    69  }
    70  
    71  func (u Upper) Start(
    72  	ctx context.Context,
    73  	args []string,
    74  	b model.TiltBuild,
    75  	fileName string,
    76  	initTerminalMode store.TerminalMode,
    77  	analyticsUserOpt analytics.Opt,
    78  	token token.Token,
    79  	cloudAddress string,
    80  ) error {
    81  
    82  	startTime := time.Now()
    83  
    84  	absTfPath, err := filepath.Abs(fileName)
    85  	if err != nil {
    86  		return err
    87  	}
    88  	return u.Init(ctx, InitAction{
    89  		TiltfilePath:     absTfPath,
    90  		UserArgs:         args,
    91  		TiltBuild:        b,
    92  		StartTime:        startTime,
    93  		AnalyticsUserOpt: analyticsUserOpt,
    94  		Token:            token,
    95  		CloudAddress:     cloudAddress,
    96  		TerminalMode:     initTerminalMode,
    97  	})
    98  }
    99  
   100  func (u Upper) Init(ctx context.Context, action InitAction) error {
   101  	u.store.Dispatch(action)
   102  	return u.store.Loop(ctx)
   103  }
   104  
   105  func upperReducerFn(ctx context.Context, state *store.EngineState, action store.Action) {
   106  	// Allow exitAction and dumpEngineStateAction even if there's a fatal error
   107  	if exitAction, isExitAction := action.(hud.ExitAction); isExitAction {
   108  		handleHudExitAction(state, exitAction)
   109  		return
   110  	}
   111  	if _, isDumpEngineStateAction := action.(hud.DumpEngineStateAction); isDumpEngineStateAction {
   112  		handleDumpEngineStateAction(ctx, state)
   113  		return
   114  	}
   115  
   116  	if state.FatalError != nil {
   117  		return
   118  	}
   119  
   120  	switch action := action.(type) {
   121  	case InitAction:
   122  		handleInitAction(ctx, state, action)
   123  	case store.ErrorAction:
   124  		state.FatalError = action.Error
   125  	case hud.ExitAction:
   126  		handleHudExitAction(state, action)
   127  
   128  	// TODO(nick): Delete these handlers in favor of the bog-standard ones that copy
   129  	// the api models directly.
   130  	case filewatch.FileWatchUpdateStatusAction:
   131  		filewatch.HandleFileWatchUpdateStatusEvent(ctx, state, action)
   132  
   133  	case k8swatch.ServiceChangeAction:
   134  		handleServiceEvent(ctx, state, action)
   135  	case store.K8sEventAction:
   136  		handleK8sEvent(ctx, state, action)
   137  	case buildcontrols.BuildCompleteAction:
   138  		buildcontrols.HandleBuildCompleted(ctx, state, action)
   139  	case buildcontrols.BuildStartedAction:
   140  		buildcontrols.HandleBuildStarted(ctx, state, action)
   141  	case ctrltiltfile.ConfigsReloadStartedAction:
   142  		ctrltiltfile.HandleConfigsReloadStarted(ctx, state, action)
   143  	case ctrltiltfile.ConfigsReloadedAction:
   144  		ctrltiltfile.HandleConfigsReloaded(ctx, state, action)
   145  	case hud.DumpEngineStateAction:
   146  		handleDumpEngineStateAction(ctx, state)
   147  	case store.AnalyticsUserOptAction:
   148  		handleAnalyticsUserOptAction(state, action)
   149  	case store.AnalyticsNudgeSurfacedAction:
   150  		handleAnalyticsNudgeSurfacedAction(ctx, state)
   151  	case store.TiltCloudStatusReceivedAction:
   152  		handleTiltCloudStatusReceivedAction(state, action)
   153  	case store.PanicAction:
   154  		handlePanicAction(state, action)
   155  	case store.LogAction:
   156  		handleLogAction(state, action)
   157  	case store.AppendToTriggerQueueAction:
   158  		state.AppendToTriggerQueue(action.Name, action.Reason)
   159  	case sessions.SessionStatusUpdateAction:
   160  		sessions.HandleSessionStatusUpdateAction(state, action)
   161  	case prompt.SwitchTerminalModeAction:
   162  		handleSwitchTerminalModeAction(state, action)
   163  	case server.OverrideTriggerModeAction:
   164  		handleOverrideTriggerModeAction(ctx, state, action)
   165  	case local.CmdCreateAction:
   166  		local.HandleCmdCreateAction(state, action)
   167  	case local.CmdUpdateStatusAction:
   168  		local.HandleCmdUpdateStatusAction(state, action)
   169  	case local.CmdDeleteAction:
   170  		local.HandleCmdDeleteAction(state, action)
   171  	case tiltfiles.TiltfileUpsertAction:
   172  		tiltfiles.HandleTiltfileUpsertAction(state, action)
   173  	case tiltfiles.TiltfileDeleteAction:
   174  		tiltfiles.HandleTiltfileDeleteAction(state, action)
   175  	case filewatches.FileWatchUpsertAction:
   176  		filewatches.HandleFileWatchUpsertAction(state, action)
   177  	case filewatches.FileWatchDeleteAction:
   178  		filewatches.HandleFileWatchDeleteAction(state, action)
   179  	case dockercomposeservices.DockerComposeServiceUpsertAction:
   180  		dockercomposeservices.HandleDockerComposeServiceUpsertAction(state, action)
   181  	case dockercomposeservices.DockerComposeServiceDeleteAction:
   182  		dockercomposeservices.HandleDockerComposeServiceDeleteAction(state, action)
   183  	case dockerimages.DockerImageUpsertAction:
   184  		dockerimages.HandleDockerImageUpsertAction(state, action)
   185  	case dockerimages.DockerImageDeleteAction:
   186  		dockerimages.HandleDockerImageDeleteAction(state, action)
   187  	case cmdimages.CmdImageUpsertAction:
   188  		cmdimages.HandleCmdImageUpsertAction(state, action)
   189  	case cmdimages.CmdImageDeleteAction:
   190  		cmdimages.HandleCmdImageDeleteAction(state, action)
   191  	case kubernetesapplys.KubernetesApplyUpsertAction:
   192  		kubernetesapplys.HandleKubernetesApplyUpsertAction(state, action)
   193  	case kubernetesapplys.KubernetesApplyDeleteAction:
   194  		kubernetesapplys.HandleKubernetesApplyDeleteAction(state, action)
   195  	case kubernetesdiscoverys.KubernetesDiscoveryUpsertAction:
   196  		kubernetesdiscoverys.HandleKubernetesDiscoveryUpsertAction(state, action)
   197  	case kubernetesdiscoverys.KubernetesDiscoveryDeleteAction:
   198  		kubernetesdiscoverys.HandleKubernetesDiscoveryDeleteAction(state, action)
   199  	case uiresources.UIResourceUpsertAction:
   200  		uiresources.HandleUIResourceUpsertAction(state, action)
   201  	case uiresources.UIResourceDeleteAction:
   202  		uiresources.HandleUIResourceDeleteAction(state, action)
   203  	case configmaps.ConfigMapUpsertAction:
   204  		configmaps.HandleConfigMapUpsertAction(state, action)
   205  	case configmaps.ConfigMapDeleteAction:
   206  		configmaps.HandleConfigMapDeleteAction(state, action)
   207  	case liveupdates.LiveUpdateUpsertAction:
   208  		liveupdates.HandleLiveUpdateUpsertAction(state, action)
   209  	case liveupdates.LiveUpdateDeleteAction:
   210  		liveupdates.HandleLiveUpdateDeleteAction(state, action)
   211  	case clusters.ClusterUpsertAction:
   212  		clusters.HandleClusterUpsertAction(state, action)
   213  	case clusters.ClusterDeleteAction:
   214  		clusters.HandleClusterDeleteAction(state, action)
   215  	case uibuttons.UIButtonUpsertAction:
   216  		uibuttons.HandleUIButtonUpsertAction(state, action)
   217  	case uibuttons.UIButtonDeleteAction:
   218  		uibuttons.HandleUIButtonDeleteAction(state, action)
   219  	case imagemaps.ImageMapUpsertAction:
   220  		imagemaps.HandleImageMapUpsertAction(state, action)
   221  	case imagemaps.ImageMapDeleteAction:
   222  		imagemaps.HandleImageMapDeleteAction(state, action)
   223  	default:
   224  		state.FatalError = fmt.Errorf("unrecognized action: %T", action)
   225  	}
   226  }
   227  
   228  var UpperReducer = store.Reducer(upperReducerFn)
   229  
   230  func handleLogAction(state *store.EngineState, action store.LogAction) {
   231  	state.LogStore.Append(action, state.Secrets)
   232  }
   233  
   234  func handleSwitchTerminalModeAction(state *store.EngineState, action prompt.SwitchTerminalModeAction) {
   235  	state.TerminalMode = action.Mode
   236  }
   237  
   238  func handleServiceEvent(ctx context.Context, state *store.EngineState, action k8swatch.ServiceChangeAction) {
   239  	service := action.Service
   240  	ms, ok := state.ManifestState(action.ManifestName)
   241  	if !ok {
   242  		return
   243  	}
   244  
   245  	runtime := ms.K8sRuntimeState()
   246  	runtime.LBs[k8s.ServiceName(service.Name)] = action.URL
   247  }
   248  
   249  func handleK8sEvent(ctx context.Context, state *store.EngineState, action store.K8sEventAction) {
   250  	// TODO(nick): I think we would so something more intelligent here, where we
   251  	// have special treatment for different types of events, e.g.:
   252  	//
   253  	// - Attach Image Pulling/Pulled events to the pod state, and display how much
   254  	//   time elapsed between them.
   255  	// - Display Node unready events as part of a health indicator, and display how
   256  	//   long it takes them to resolve.
   257  	handleLogAction(state, action.ToLogAction(action.ManifestName))
   258  }
   259  
   260  func handleDumpEngineStateAction(ctx context.Context, engineState *store.EngineState) {
   261  	f, err := os.CreateTemp("", "tilt-engine-state-*.txt")
   262  	if err != nil {
   263  		logger.Get(ctx).Infof("error creating temp file to write engine state: %v", err)
   264  		return
   265  	}
   266  
   267  	logger.Get(ctx).Infof("dumped tilt engine state to %q", f.Name())
   268  	spew.Fdump(f, engineState)
   269  
   270  	err = f.Close()
   271  	if err != nil {
   272  		logger.Get(ctx).Infof("error closing engine state temp file: %v", err)
   273  		return
   274  	}
   275  }
   276  
   277  func handleInitAction(ctx context.Context, engineState *store.EngineState, action InitAction) {
   278  	engineState.TiltBuildInfo = action.TiltBuild
   279  	engineState.TiltStartTime = action.StartTime
   280  	engineState.DesiredTiltfilePath = action.TiltfilePath
   281  	engineState.UserConfigState = model.NewUserConfigState(action.UserArgs)
   282  	engineState.AnalyticsUserOpt = action.AnalyticsUserOpt
   283  	engineState.CloudAddress = action.CloudAddress
   284  	engineState.Token = action.Token
   285  	engineState.TerminalMode = action.TerminalMode
   286  }
   287  
   288  func handleHudExitAction(state *store.EngineState, action hud.ExitAction) {
   289  	if action.Err != nil {
   290  		state.FatalError = action.Err
   291  	} else {
   292  		state.UserExited = true
   293  	}
   294  }
   295  
   296  func handlePanicAction(state *store.EngineState, action store.PanicAction) {
   297  	state.PanicExited = action.Err
   298  }
   299  
   300  func handleAnalyticsUserOptAction(state *store.EngineState, action store.AnalyticsUserOptAction) {
   301  	state.AnalyticsUserOpt = action.Opt
   302  }
   303  
   304  // The first time we hear that the analytics nudge was surfaced, record a metric.
   305  // We double check !state.AnalyticsNudgeSurfaced -- i.e. that the state doesn't
   306  // yet know that we've surfaced the nudge -- to ensure that we only record this
   307  // metric once (since it's an anonymous metric, we can't slice it by e.g. # unique
   308  // users, so the numbers need to be as accurate as possible).
   309  func handleAnalyticsNudgeSurfacedAction(ctx context.Context, state *store.EngineState) {
   310  	if !state.AnalyticsNudgeSurfaced {
   311  		tiltanalytics.Get(ctx).Incr("analytics.nudge.surfaced", nil)
   312  		state.AnalyticsNudgeSurfaced = true
   313  	}
   314  }
   315  
   316  func handleTiltCloudStatusReceivedAction(state *store.EngineState, action store.TiltCloudStatusReceivedAction) {
   317  	state.SuggestedTiltVersion = action.SuggestedTiltVersion
   318  }
   319  
   320  func handleOverrideTriggerModeAction(ctx context.Context, state *store.EngineState,
   321  	action server.OverrideTriggerModeAction) {
   322  	// TODO(maia): in this implementation, overrides do NOT persist across Tiltfile loads
   323  	//   (i.e. the next Tiltfile load will wipe out the override we just put in place).
   324  	//   If we want to keep this functionality, the next step is to store the set of overrides
   325  	//   on the engine state, and whenever we load the manifest from the Tiltfile, apply
   326  	//   any necessary overrides.
   327  
   328  	// We validate trigger mode when we receive a request, so this should never happen
   329  	if !model.ValidTriggerMode(action.TriggerMode) {
   330  		logger.Get(ctx).Errorf("INTERNAL ERROR overriding trigger mode: invalid trigger mode %d", action.TriggerMode)
   331  		return
   332  	}
   333  
   334  	for _, mName := range action.ManifestNames {
   335  		mt, ok := state.ManifestTargets[mName]
   336  		if !ok {
   337  			// We validate manifest names when we receive a request, so this should never happen
   338  			logger.Get(ctx).Errorf("INTERNAL ERROR overriding trigger mode: no such manifest %q", mName)
   339  			return
   340  		}
   341  		mt.Manifest.TriggerMode = action.TriggerMode
   342  	}
   343  }