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