github.com/grahambrereton-form3/tilt@v0.10.18/internal/store/engine_state.go (about) 1 package store 2 3 import ( 4 "fmt" 5 "os" 6 "path/filepath" 7 "sort" 8 "time" 9 10 "github.com/windmilleng/wmclient/pkg/analytics" 11 12 tiltanalytics "github.com/windmilleng/tilt/internal/analytics" 13 "github.com/windmilleng/tilt/internal/container" 14 "github.com/windmilleng/tilt/internal/dockercompose" 15 "github.com/windmilleng/tilt/internal/hud/view" 16 "github.com/windmilleng/tilt/internal/ospath" 17 "github.com/windmilleng/tilt/internal/token" 18 "github.com/windmilleng/tilt/pkg/model" 19 ) 20 21 type EngineState struct { 22 TiltBuildInfo model.TiltBuild 23 TiltStartTime time.Time 24 25 // saved so that we can render in order 26 ManifestDefinitionOrder []model.ManifestName 27 28 // TODO(nick): This will eventually be a general Target index. 29 ManifestTargets map[model.ManifestName]*ManifestTarget 30 31 CurrentlyBuilding model.ManifestName 32 WatchFiles bool 33 34 // How many builds have been completed (pass or fail) since starting tilt 35 CompletedBuildCount int 36 37 // For synchronizing BuildController so that it's only 38 // doing one action at a time. In the future, we might 39 // want to allow it to parallelize builds better, but that 40 // would require better tools for triaging output to different streams. 41 BuildControllerActionCount int 42 43 FatalError error 44 HUDEnabled bool 45 46 // The user has indicated they want to exit 47 UserExited bool 48 49 // The full log stream for tilt. This might deserve gc or file storage at some point. 50 Log model.Log `testdiff:"ignore"` 51 52 TiltfilePath string 53 ConfigFiles []string 54 TiltIgnoreContents string 55 PendingConfigFileChanges map[string]time.Time 56 57 // InitManifests is the list of manifest names that we were told to init from the CLI. 58 InitManifests []model.ManifestName 59 60 TriggerQueue []model.ManifestName 61 62 LogTimestamps bool 63 IsProfiling bool 64 65 TiltfileState ManifestState 66 67 // from GitHub 68 LatestTiltBuild model.TiltBuild 69 70 // Analytics Info 71 72 AnalyticsUserOpt analytics.Opt // changes to this field will propagate into the TiltAnalytics subscriber + we'll record them as user choice 73 AnalyticsTiltfileOpt analytics.Opt // Set by the Tiltfile. Overrides the UserOpt. 74 AnalyticsNudgeSurfaced bool // this flag is set the first time we show the analytics nudge to the user. 75 76 Features map[string]bool 77 78 Secrets model.SecretSet 79 80 CloudAddress string 81 Token token.Token 82 TeamName string 83 84 TiltCloudUsername string 85 TokenKnownUnregistered bool // to distinguish whether an empty TiltCloudUsername means "we haven't checked" or "we checked and the token isn't registered" 86 WaitingForTiltCloudUsernamePostRegistration bool 87 88 DockerPruneSettings model.DockerPruneSettings 89 } 90 91 // Merge analytics opt-in status from different sources. 92 // The Tiltfile opt-in takes precedence over the user opt-in. 93 func (e *EngineState) AnalyticsEffectiveOpt() analytics.Opt { 94 if tiltanalytics.IsAnalyticsDisabledFromEnv() { 95 return analytics.OptOut 96 } 97 if e.AnalyticsTiltfileOpt != analytics.OptDefault { 98 return e.AnalyticsTiltfileOpt 99 } 100 return e.AnalyticsUserOpt 101 } 102 103 func (e *EngineState) ManifestNamesForTargetID(id model.TargetID) []model.ManifestName { 104 result := make([]model.ManifestName, 0) 105 for mn, mt := range e.ManifestTargets { 106 manifest := mt.Manifest 107 for _, iTarget := range manifest.ImageTargets { 108 if iTarget.ID() == id { 109 result = append(result, mn) 110 } 111 } 112 if manifest.K8sTarget().ID() == id { 113 result = append(result, mn) 114 } 115 if manifest.DockerComposeTarget().ID() == id { 116 result = append(result, mn) 117 } 118 if manifest.LocalTarget().ID() == id { 119 result = append(result, mn) 120 } 121 } 122 return result 123 } 124 125 func (e *EngineState) BuildStatus(id model.TargetID) BuildStatus { 126 mns := e.ManifestNamesForTargetID(id) 127 for _, mn := range mns { 128 ms := e.ManifestTargets[mn].State 129 bs := ms.BuildStatus(id) 130 if !bs.IsEmpty() { 131 return bs 132 } 133 } 134 return BuildStatus{} 135 } 136 137 func (e *EngineState) UpsertManifestTarget(mt *ManifestTarget) { 138 mn := mt.Manifest.Name 139 _, ok := e.ManifestTargets[mn] 140 if !ok { 141 e.ManifestDefinitionOrder = append(e.ManifestDefinitionOrder, mn) 142 } 143 e.ManifestTargets[mn] = mt 144 } 145 146 func (e EngineState) Manifest(mn model.ManifestName) (model.Manifest, bool) { 147 m, ok := e.ManifestTargets[mn] 148 if !ok { 149 return model.Manifest{}, ok 150 } 151 return m.Manifest, ok 152 } 153 154 func (e EngineState) ManifestState(mn model.ManifestName) (*ManifestState, bool) { 155 m, ok := e.ManifestTargets[mn] 156 if !ok { 157 return nil, ok 158 } 159 return m.State, ok 160 } 161 162 // Returns Manifests in a stable order 163 func (e EngineState) Manifests() []model.Manifest { 164 result := make([]model.Manifest, 0, len(e.ManifestTargets)) 165 for _, mn := range e.ManifestDefinitionOrder { 166 mt, ok := e.ManifestTargets[mn] 167 if !ok { 168 continue 169 } 170 result = append(result, mt.Manifest) 171 } 172 return result 173 } 174 175 // Returns ManifestStates in a stable order 176 func (e EngineState) ManifestStates() []*ManifestState { 177 result := make([]*ManifestState, 0, len(e.ManifestTargets)) 178 for _, mn := range e.ManifestDefinitionOrder { 179 mt, ok := e.ManifestTargets[mn] 180 if !ok { 181 continue 182 } 183 result = append(result, mt.State) 184 } 185 return result 186 } 187 188 // Returns ManifestTargets in a stable order 189 func (e EngineState) Targets() []*ManifestTarget { 190 result := make([]*ManifestTarget, 0, len(e.ManifestTargets)) 191 for _, mn := range e.ManifestDefinitionOrder { 192 mt, ok := e.ManifestTargets[mn] 193 if !ok { 194 continue 195 } 196 result = append(result, mt) 197 } 198 return result 199 } 200 201 func (e *EngineState) ManifestInTriggerQueue(mn model.ManifestName) bool { 202 for _, queued := range e.TriggerQueue { 203 if queued == mn { 204 return true 205 } 206 } 207 return false 208 } 209 210 func (e EngineState) RelativeTiltfilePath() (string, error) { 211 wd, err := os.Getwd() 212 if err != nil { 213 return "", err 214 } 215 return filepath.Rel(wd, e.TiltfilePath) 216 } 217 218 func (e EngineState) IsEmpty() bool { 219 return len(e.ManifestTargets) == 0 220 } 221 222 func (e EngineState) LastTiltfileError() error { 223 return e.TiltfileState.LastBuild().Error 224 } 225 226 func (e *EngineState) HasDockerBuild() bool { 227 for _, m := range e.Manifests() { 228 for _, targ := range m.ImageTargets { 229 if targ.IsDockerBuild() { 230 return true 231 } 232 } 233 } 234 return false 235 } 236 237 func (e *EngineState) InitialBuildsCompleted() bool { 238 if e.ManifestTargets == nil || len(e.ManifestTargets) == 0 { 239 return false 240 } 241 242 for _, mt := range e.ManifestTargets { 243 if !mt.Manifest.TriggerMode.AutoInitial() { 244 continue 245 } 246 247 ms, _ := e.ManifestState(mt.Manifest.Name) 248 if ms == nil || ms.LastBuild().Empty() { 249 return false 250 } 251 } 252 253 return true 254 } 255 256 // TODO(nick): This will eventually implement TargetStatus 257 type BuildStatus struct { 258 // Stores the times of all the pending changes, 259 // so we can prioritize the oldest one first. 260 // This map is mutable. 261 PendingFileChanges map[string]time.Time 262 263 LastSuccessfulResult BuildResult 264 LastResult BuildResult 265 } 266 267 func newBuildStatus() *BuildStatus { 268 return &BuildStatus{ 269 PendingFileChanges: make(map[string]time.Time), 270 } 271 } 272 273 func (s BuildStatus) IsEmpty() bool { 274 return len(s.PendingFileChanges) == 0 && s.LastSuccessfulResult == nil 275 } 276 277 type ManifestState struct { 278 Name model.ManifestName 279 280 BuildStatuses map[model.TargetID]*BuildStatus 281 RuntimeState RuntimeState 282 283 PendingManifestChange time.Time 284 285 // The current build 286 CurrentBuild model.BuildRecord 287 288 LastSuccessfulDeployTime time.Time 289 290 // The last `BuildHistoryLimit` builds. The most recent build is first in the slice. 291 BuildHistory []model.BuildRecord 292 293 // The container IDs that we've run a LiveUpdate on, if any. Their contents have 294 // diverged from the image they are built on. If these container don't appear on 295 // the pod, we've lost that state and need to rebuild. 296 LiveUpdatedContainerIDs map[container.ID]bool 297 298 // We detected stale code and are currently doing an image build 299 NeedsRebuildFromCrash bool 300 301 // If a pod had to be killed because it was crashing, we keep the old log 302 // around for a little while so we can show it in the UX. 303 CrashLog model.Log 304 305 // The log stream for this resource 306 CombinedLog model.Log `testdiff:"ignore"` 307 308 // If this manifest was changed, which config files led to the most recent change in manifest definition 309 ConfigFilesThatCausedChange []string 310 } 311 312 func NewState() *EngineState { 313 ret := &EngineState{} 314 ret.Log = model.Log{} 315 ret.ManifestTargets = make(map[model.ManifestName]*ManifestTarget) 316 ret.PendingConfigFileChanges = make(map[string]time.Time) 317 ret.Secrets = model.SecretSet{} 318 ret.DockerPruneSettings = model.DefaultDockerPruneSettings() 319 return ret 320 } 321 322 func newManifestState(mn model.ManifestName) *ManifestState { 323 return &ManifestState{ 324 Name: mn, 325 BuildStatuses: make(map[model.TargetID]*BuildStatus), 326 LiveUpdatedContainerIDs: container.NewIDSet(), 327 } 328 } 329 330 func (ms *ManifestState) TargetID() model.TargetID { 331 return model.TargetID{ 332 Type: model.TargetTypeManifest, 333 Name: ms.Name.TargetName(), 334 } 335 } 336 337 func (ms *ManifestState) BuildStatus(id model.TargetID) BuildStatus { 338 result, ok := ms.BuildStatuses[id] 339 if !ok { 340 return BuildStatus{} 341 } 342 return *result 343 } 344 345 func (ms *ManifestState) MutableBuildStatus(id model.TargetID) *BuildStatus { 346 result, ok := ms.BuildStatuses[id] 347 if !ok { 348 result = newBuildStatus() 349 ms.BuildStatuses[id] = result 350 } 351 return result 352 } 353 354 func (ms *ManifestState) DCRuntimeState() dockercompose.State { 355 ret, _ := ms.RuntimeState.(dockercompose.State) 356 return ret 357 } 358 359 func (ms *ManifestState) IsDC() bool { 360 _, ok := ms.RuntimeState.(dockercompose.State) 361 return ok 362 } 363 364 func (ms *ManifestState) K8sRuntimeState() K8sRuntimeState { 365 ret, _ := ms.RuntimeState.(K8sRuntimeState) 366 return ret 367 } 368 369 func (ms *ManifestState) GetOrCreateK8sRuntimeState() K8sRuntimeState { 370 ret, ok := ms.RuntimeState.(K8sRuntimeState) 371 if !ok { 372 ret = NewK8sRuntimeState() 373 ms.RuntimeState = ret 374 } 375 return ret 376 } 377 378 func (ms *ManifestState) IsK8s() bool { 379 _, ok := ms.RuntimeState.(K8sRuntimeState) 380 return ok 381 } 382 383 func (ms *ManifestState) ActiveBuild() model.BuildRecord { 384 return ms.CurrentBuild 385 } 386 387 func (ms *ManifestState) LastBuild() model.BuildRecord { 388 if len(ms.BuildHistory) == 0 { 389 return model.BuildRecord{} 390 } 391 return ms.BuildHistory[0] 392 } 393 394 func (ms *ManifestState) AddCompletedBuild(bs model.BuildRecord) { 395 ms.BuildHistory = append([]model.BuildRecord{bs}, ms.BuildHistory...) 396 if len(ms.BuildHistory) > model.BuildHistoryLimit { 397 ms.BuildHistory = ms.BuildHistory[:model.BuildHistoryLimit] 398 } 399 } 400 401 func (ms *ManifestState) StartedFirstBuild() bool { 402 return !ms.CurrentBuild.Empty() || len(ms.BuildHistory) > 0 403 } 404 405 func (ms *ManifestState) MostRecentPod() Pod { 406 return ms.K8sRuntimeState().MostRecentPod() 407 } 408 409 func (ms *ManifestState) HasPendingFileChanges() bool { 410 for _, status := range ms.BuildStatuses { 411 if len(status.PendingFileChanges) > 0 { 412 return true 413 } 414 } 415 return false 416 } 417 418 func (ms *ManifestState) NextBuildReason() model.BuildReason { 419 reason := model.BuildReasonNone 420 if ms.HasPendingFileChanges() { 421 reason = reason.With(model.BuildReasonFlagChangedFiles) 422 } 423 if !ms.PendingManifestChange.IsZero() { 424 reason = reason.With(model.BuildReasonFlagConfig) 425 } 426 if !ms.StartedFirstBuild() { 427 reason = reason.With(model.BuildReasonFlagInit) 428 } 429 if ms.NeedsRebuildFromCrash { 430 reason = reason.With(model.BuildReasonFlagCrash) 431 } 432 return reason 433 } 434 435 // Whether a change at the given time should trigger a build. 436 // Used to determine if changes to synced files or config files 437 // should kick off a new build. 438 func (ms *ManifestState) IsPendingTime(t time.Time) bool { 439 return !t.IsZero() && t.After(ms.LastBuild().StartTime) 440 } 441 442 // Whether changes have been made to this Manifest's synced files 443 // or config since the last build. 444 // 445 // Returns: 446 // bool: whether changes have been made 447 // Time: the time of the earliest change 448 func (ms *ManifestState) HasPendingChanges() (bool, time.Time) { 449 return ms.HasPendingChangesBefore(time.Now()) 450 } 451 452 // Like HasPendingChanges, but relative to a particular time. 453 func (ms *ManifestState) HasPendingChangesBefore(highWaterMark time.Time) (bool, time.Time) { 454 ok := false 455 earliest := highWaterMark 456 t := ms.PendingManifestChange 457 if t.Before(earliest) && ms.IsPendingTime(t) { 458 ok = true 459 earliest = t 460 } 461 462 for _, status := range ms.BuildStatuses { 463 for _, t := range status.PendingFileChanges { 464 if t.Before(earliest) && ms.IsPendingTime(t) { 465 ok = true 466 earliest = t 467 } 468 } 469 } 470 if !ok { 471 return ok, time.Time{} 472 } 473 return ok, earliest 474 } 475 476 var _ model.TargetStatus = &ManifestState{} 477 478 type YAMLManifestState struct { 479 HasBeenDeployed bool 480 481 CurrentApplyStartTime time.Time 482 LastError error 483 LastApplyFinishTime time.Time 484 LastSuccessfulApplyTime time.Time 485 LastApplyStartTime time.Time 486 } 487 488 func NewYAMLManifestState() *YAMLManifestState { 489 return &YAMLManifestState{} 490 } 491 492 func (s *YAMLManifestState) TargetID() model.TargetID { 493 return model.TargetID{ 494 Type: model.TargetTypeManifest, 495 Name: model.UnresourcedYAMLManifestName.TargetName(), 496 } 497 } 498 499 func (s *YAMLManifestState) ActiveBuild() model.BuildRecord { 500 return model.BuildRecord{ 501 StartTime: s.CurrentApplyStartTime, 502 } 503 } 504 505 func (s *YAMLManifestState) LastBuild() model.BuildRecord { 506 return model.BuildRecord{ 507 StartTime: s.LastApplyStartTime, 508 FinishTime: s.LastApplyFinishTime, 509 Error: s.LastError, 510 } 511 } 512 513 var _ model.TargetStatus = &YAMLManifestState{} 514 515 func ManifestTargetEndpoints(mt *ManifestTarget) (endpoints []string) { 516 defer func() { 517 sort.Strings(endpoints) 518 }() 519 520 // If the user specified port-forwards in the Tiltfile, we 521 // assume that's what they want to see in the UI 522 portForwards := mt.Manifest.K8sTarget().PortForwards 523 if len(portForwards) > 0 { 524 for _, pf := range portForwards { 525 endpoints = append(endpoints, fmt.Sprintf("http://localhost:%d/", pf.LocalPort)) 526 } 527 return endpoints 528 } 529 530 publishedPorts := mt.Manifest.DockerComposeTarget().PublishedPorts() 531 if len(publishedPorts) > 0 { 532 for _, p := range publishedPorts { 533 endpoints = append(endpoints, fmt.Sprintf("http://localhost:%d/", p)) 534 } 535 return endpoints 536 } 537 538 for _, u := range mt.State.K8sRuntimeState().LBs { 539 if u != nil { 540 endpoints = append(endpoints, u.String()) 541 } 542 } 543 return endpoints 544 } 545 546 func StateToView(s EngineState) view.View { 547 ret := view.View{ 548 IsProfiling: s.IsProfiling, 549 LogTimestamps: s.LogTimestamps, 550 } 551 552 ret.Resources = append(ret.Resources, tiltfileResourceView(s)) 553 554 for _, name := range s.ManifestDefinitionOrder { 555 mt, ok := s.ManifestTargets[name] 556 if !ok { 557 continue 558 } 559 560 ms := mt.State 561 562 var absWatchDirs []string 563 var absWatchPaths []string 564 for _, p := range mt.Manifest.LocalPaths() { 565 fi, err := os.Stat(p) 566 if err == nil && !fi.IsDir() { 567 absWatchPaths = append(absWatchPaths, p) 568 } else { 569 absWatchDirs = append(absWatchDirs, p) 570 } 571 } 572 absWatchPaths = append(absWatchPaths, s.TiltfilePath) 573 relWatchDirs := ospath.TryAsCwdChildren(absWatchDirs) 574 relWatchPaths := ospath.TryAsCwdChildren(absWatchPaths) 575 576 var pendingBuildEdits []string 577 for _, status := range ms.BuildStatuses { 578 for f := range status.PendingFileChanges { 579 pendingBuildEdits = append(pendingBuildEdits, f) 580 } 581 } 582 583 pendingBuildEdits = ospath.FileListDisplayNames(absWatchDirs, pendingBuildEdits) 584 585 buildHistory := append([]model.BuildRecord{}, ms.BuildHistory...) 586 for i, build := range buildHistory { 587 build.Edits = ospath.FileListDisplayNames(absWatchDirs, build.Edits) 588 buildHistory[i] = build 589 } 590 591 currentBuild := ms.CurrentBuild 592 currentBuild.Edits = ospath.FileListDisplayNames(absWatchDirs, ms.CurrentBuild.Edits) 593 594 // Sort the strings to make the outputs deterministic. 595 sort.Strings(pendingBuildEdits) 596 597 endpoints := ManifestTargetEndpoints(mt) 598 599 // NOTE(nick): Right now, the UX is designed to show the output exactly one 600 // pod. A better UI might summarize the pods in other ways (e.g., show the 601 // "most interesting" pod that's crash looping, or show logs from all pods 602 // at once). 603 _, pendingBuildSince := ms.HasPendingChanges() 604 r := view.Resource{ 605 Name: name, 606 DirectoriesWatched: relWatchDirs, 607 PathsWatched: relWatchPaths, 608 LastDeployTime: ms.LastSuccessfulDeployTime, 609 TriggerMode: mt.Manifest.TriggerMode, 610 BuildHistory: buildHistory, 611 PendingBuildEdits: pendingBuildEdits, 612 PendingBuildSince: pendingBuildSince, 613 PendingBuildReason: ms.NextBuildReason(), 614 CurrentBuild: currentBuild, 615 CrashLog: ms.CrashLog, 616 Endpoints: endpoints, 617 ResourceInfo: resourceInfoView(mt), 618 } 619 620 ret.Resources = append(ret.Resources, r) 621 } 622 623 ret.Log = s.Log 624 ret.FatalError = s.FatalError 625 626 return ret 627 } 628 629 const TiltfileManifestName = model.ManifestName("(Tiltfile)") 630 631 func tiltfileResourceView(s EngineState) view.Resource { 632 tr := view.Resource{ 633 Name: TiltfileManifestName, 634 IsTiltfile: true, 635 CurrentBuild: s.TiltfileState.CurrentBuild, 636 BuildHistory: s.TiltfileState.BuildHistory, 637 } 638 if !s.TiltfileState.CurrentBuild.Empty() { 639 tr.PendingBuildSince = s.TiltfileState.CurrentBuild.StartTime 640 } else { 641 tr.LastDeployTime = s.TiltfileState.LastBuild().FinishTime 642 } 643 if !s.TiltfileState.LastBuild().Empty() { 644 err := s.TiltfileState.LastBuild().Error 645 if err != nil { 646 tr.CrashLog = model.NewLog(err.Error()) 647 } 648 } 649 return tr 650 } 651 652 func resourceInfoView(mt *ManifestTarget) view.ResourceInfoView { 653 if mt.Manifest.IsUnresourcedYAMLManifest() { 654 return view.YAMLResourceInfo{ 655 K8sResources: mt.Manifest.K8sTarget().DisplayNames, 656 } 657 } 658 659 switch state := mt.State.RuntimeState.(type) { 660 case dockercompose.State: 661 return view.NewDCResourceInfo(mt.Manifest.DockerComposeTarget().ConfigPaths, 662 state.Status, state.ContainerID, state.Log(), state.StartTime) 663 case K8sRuntimeState: 664 pod := state.MostRecentPod() 665 return view.K8sResourceInfo{ 666 PodName: pod.PodID.String(), 667 PodCreationTime: pod.StartedAt, 668 PodUpdateStartTime: pod.UpdateStartTime, 669 PodStatus: pod.Status, 670 PodRestarts: pod.VisibleContainerRestarts(), 671 PodLog: pod.CurrentLog, 672 } 673 case LocalRuntimeState: 674 return view.LocalResourceInfo{} 675 default: 676 // This is silly but it was the old behavior. 677 return view.K8sResourceInfo{} 678 } 679 } 680 681 // DockerComposeConfigPath returns the path to the docker-compose yaml file of any 682 // docker-compose manifests on this EngineState. 683 // NOTE(maia): current assumption is only one d-c.yaml per run, so we take the 684 // path from the first d-c manifest we see. 685 func (s EngineState) DockerComposeConfigPath() []string { 686 for _, mt := range s.ManifestTargets { 687 if mt.Manifest.IsDC() { 688 return mt.Manifest.DockerComposeTarget().ConfigPaths 689 } 690 } 691 return []string{} 692 }