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 }