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 }