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 }