github.com/tilt-dev/tilt@v0.36.0/internal/store/engine_state.go (about) 1 package store 2 3 import ( 4 "fmt" 5 "iter" 6 "sort" 7 "time" 8 9 "github.com/tilt-dev/wmclient/pkg/analytics" 10 11 tiltanalytics "github.com/tilt-dev/tilt/internal/analytics" 12 "github.com/tilt-dev/tilt/internal/dockercompose" 13 "github.com/tilt-dev/tilt/internal/k8s" 14 "github.com/tilt-dev/tilt/internal/store/k8sconv" 15 "github.com/tilt-dev/tilt/internal/timecmp" 16 "github.com/tilt-dev/tilt/internal/token" 17 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 18 "github.com/tilt-dev/tilt/pkg/model" 19 "github.com/tilt-dev/tilt/pkg/model/logstore" 20 ) 21 22 type EngineState struct { 23 TiltBuildInfo model.TiltBuild 24 TiltStartTime time.Time 25 26 // saved so that we can render in order 27 ManifestDefinitionOrder []model.ManifestName 28 29 // TODO(nick): This will eventually be a general Target index. 30 ManifestTargets map[model.ManifestName]*ManifestTarget 31 32 // Keep a set of the current builds, so we can quickly count how many 33 // builds there are without looking at all builds in the list. 34 CurrentBuildSet map[model.ManifestName]bool 35 36 TerminalMode TerminalMode 37 38 // For synchronizing BuildController -- wait until engine records all builds started 39 // so far before starting another build 40 BuildControllerStartCount int 41 42 // How many builds have been completed (pass or fail) since starting tilt 43 CompletedBuildCount int 44 45 UpdateSettings model.UpdateSettings 46 47 FatalError error 48 49 // The user has indicated they want to exit 50 UserExited bool 51 52 // We recovered from a panic(). We need to clean up the RTY and print the error. 53 PanicExited error 54 55 // Normal process termination. Either Tilt completed all its work, 56 // or it determined that it was unable to complete the work it was assigned. 57 // 58 // Note that ExitSignal/ExitError is never triggered in normal 59 // 'tilt up`/dev mode. It's more for CI modes and tilt up --watch=false modes. 60 // 61 // We don't provide the ability to customize exit codes. Either the 62 // process exited successfully, or with an error. In the future, we might 63 // add the ability to put an exit code in the error. 64 ExitSignal bool 65 ExitError error 66 67 // All logs in Tilt, stored in a structured format. 68 LogStore *logstore.LogStore `testdiff:"ignore"` 69 70 TriggerQueue []model.ManifestName 71 72 TiltfileDefinitionOrder []model.ManifestName 73 TiltfileStates map[model.ManifestName]*ManifestState 74 75 SuggestedTiltVersion string 76 VersionSettings model.VersionSettings 77 78 // Analytics Info 79 AnalyticsEnvOpt analytics.Opt 80 AnalyticsUserOpt analytics.Opt // changes to this field will propagate into the TiltAnalytics subscriber + we'll record them as user choice 81 AnalyticsTiltfileOpt analytics.Opt // Set by the Tiltfile. Overrides the UserOpt. 82 AnalyticsNudgeSurfaced bool // this flag is set the first time we show the analytics nudge to the user. 83 84 Features map[string]bool 85 86 Secrets model.SecretSet 87 88 CloudAddress string 89 Token token.Token 90 TeamID string 91 92 DockerPruneSettings model.DockerPruneSettings 93 94 TelemetrySettings model.TelemetrySettings 95 96 UserConfigState model.UserConfigState 97 98 // The initialization sequence is unfortunate. Currently we have: 99 // 1) Dispatch an InitAction 100 // 1) InitAction sets DesiredTiltfilePath 101 // 2) ConfigsController reads DesiredTiltfilePath, writes a new Tiltfile object to the APIServer 102 // 4) ConfigsController dispatches a TiltfileCreateAction, to copy the apiserver data into the EngineState 103 DesiredTiltfilePath string 104 105 // KubernetesResources by name. 106 // Updated to match KubernetesApply + KubernetesDiscovery 107 KubernetesResources map[string]*k8sconv.KubernetesResource `json:"-"` 108 109 // API-server-based data models. Stored in EngineState 110 // to assist in migration. 111 Cmds map[string]*Cmd `json:"-"` 112 Tiltfiles map[string]*v1alpha1.Tiltfile `json:"-"` 113 FileWatches map[string]*v1alpha1.FileWatch `json:"-"` 114 KubernetesApplys map[string]*v1alpha1.KubernetesApply `json:"-"` 115 KubernetesDiscoverys map[string]*v1alpha1.KubernetesDiscovery `json:"-"` 116 UIResources map[string]*v1alpha1.UIResource `json:"-"` 117 ConfigMaps map[string]*v1alpha1.ConfigMap `json:"-"` 118 LiveUpdates map[string]*v1alpha1.LiveUpdate `json:"-"` 119 Clusters map[string]*v1alpha1.Cluster `json:"-"` 120 UIButtons map[string]*v1alpha1.UIButton `json:"-"` 121 DockerComposeServices map[string]*v1alpha1.DockerComposeService `json:"-"` 122 ImageMaps map[string]*v1alpha1.ImageMap `json:"-"` 123 DockerImages map[string]*v1alpha1.DockerImage `json:"-"` 124 CmdImages map[string]*v1alpha1.CmdImage `json:"-"` 125 } 126 127 func (e *EngineState) MainTiltfilePath() string { 128 tf, ok := e.Tiltfiles[model.MainTiltfileManifestName.String()] 129 if !ok { 130 return "" 131 } 132 return tf.Spec.Path 133 } 134 135 // Merge analytics opt-in status from different sources. 136 // The Tiltfile opt-in takes precedence over the user opt-in. 137 func (e *EngineState) AnalyticsEffectiveOpt() analytics.Opt { 138 if e.AnalyticsEnvOpt != analytics.OptDefault { 139 return e.AnalyticsEnvOpt 140 } 141 if e.AnalyticsTiltfileOpt != analytics.OptDefault { 142 return e.AnalyticsTiltfileOpt 143 } 144 return e.AnalyticsUserOpt 145 } 146 147 func (e *EngineState) ManifestNamesForTargetID(id model.TargetID) []model.ManifestName { 148 if id.Type == model.TargetTypeConfigs { 149 return []model.ManifestName{model.ManifestName(id.Name)} 150 } 151 152 result := make([]model.ManifestName, 0) 153 for mn, mt := range e.ManifestTargets { 154 manifest := mt.Manifest 155 for _, iTarget := range manifest.ImageTargets { 156 if iTarget.ID() == id { 157 result = append(result, mn) 158 } 159 } 160 if manifest.K8sTarget().ID() == id { 161 result = append(result, mn) 162 } 163 if manifest.DockerComposeTarget().ID() == id { 164 result = append(result, mn) 165 } 166 if manifest.LocalTarget().ID() == id { 167 result = append(result, mn) 168 } 169 } 170 return result 171 } 172 173 func (e *EngineState) IsBuilding(name model.ManifestName) bool { 174 ms, ok := e.ManifestState(name) 175 if !ok { 176 return false 177 } 178 return ms.IsBuilding() 179 } 180 181 // Find the first build status. Only suitable for testing. 182 func (e *EngineState) BuildStatus(id model.TargetID) *BuildStatus { 183 mns := e.ManifestNamesForTargetID(id) 184 for _, mn := range mns { 185 ms := e.ManifestTargets[mn].State 186 bs, ok := ms.BuildStatus(id) 187 if ok && !bs.IsEmpty() { 188 return bs 189 } 190 } 191 return &BuildStatus{} 192 } 193 194 func (e *EngineState) AvailableBuildSlots() int { 195 currentBuildCount := len(e.CurrentBuildSet) 196 if currentBuildCount >= e.UpdateSettings.MaxParallelUpdates() { 197 // this could happen if user decreases max build slots while 198 // multiple builds are in progress, no big deal 199 return 0 200 } 201 return e.UpdateSettings.MaxParallelUpdates() - currentBuildCount 202 } 203 204 func (e *EngineState) UpsertManifestTarget(mt *ManifestTarget) { 205 mn := mt.Manifest.Name 206 _, ok := e.ManifestTargets[mn] 207 if !ok { 208 e.ManifestDefinitionOrder = append(e.ManifestDefinitionOrder, mn) 209 } 210 e.ManifestTargets[mn] = mt 211 } 212 213 func (e *EngineState) RemoveManifestTarget(mn model.ManifestName) { 214 delete(e.ManifestTargets, mn) 215 newOrder := []model.ManifestName{} 216 for _, n := range e.ManifestDefinitionOrder { 217 if n == mn { 218 continue 219 } 220 newOrder = append(newOrder, n) 221 } 222 e.ManifestDefinitionOrder = newOrder 223 } 224 225 func (e EngineState) Manifest(mn model.ManifestName) (model.Manifest, bool) { 226 m, ok := e.ManifestTargets[mn] 227 if !ok { 228 return model.Manifest{}, ok 229 } 230 return m.Manifest, ok 231 } 232 233 func (e EngineState) ManifestState(mn model.ManifestName) (*ManifestState, bool) { 234 st, ok := e.TiltfileStates[mn] 235 if ok { 236 return st, ok 237 } 238 239 m, ok := e.ManifestTargets[mn] 240 if !ok { 241 return nil, ok 242 } 243 return m.State, ok 244 } 245 246 // Returns Manifests in a stable order 247 func (e EngineState) Manifests() []model.Manifest { 248 result := make([]model.Manifest, 0, len(e.ManifestTargets)) 249 for _, mn := range e.ManifestDefinitionOrder { 250 mt, ok := e.ManifestTargets[mn] 251 if !ok { 252 continue 253 } 254 result = append(result, mt.Manifest) 255 } 256 return result 257 } 258 259 // Returns ManifestStates in a stable order 260 func (e EngineState) ManifestStates() []*ManifestState { 261 result := make([]*ManifestState, 0, len(e.ManifestTargets)) 262 for _, mn := range e.ManifestDefinitionOrder { 263 mt, ok := e.ManifestTargets[mn] 264 if !ok { 265 continue 266 } 267 result = append(result, mt.State) 268 } 269 return result 270 } 271 272 // Returns ManifestTargets in a stable order 273 func (e EngineState) Targets() []*ManifestTarget { 274 result := make([]*ManifestTarget, 0, len(e.ManifestTargets)) 275 for _, mn := range e.ManifestDefinitionOrder { 276 mt, ok := e.ManifestTargets[mn] 277 if !ok { 278 continue 279 } 280 result = append(result, mt) 281 } 282 return result 283 } 284 285 // Returns TiltfileStates in a stable order. 286 func (e EngineState) GetTiltfileStates() []*ManifestState { 287 result := make([]*ManifestState, 0, len(e.TiltfileStates)) 288 for _, mn := range e.TiltfileDefinitionOrder { 289 mt, ok := e.TiltfileStates[mn] 290 if !ok { 291 continue 292 } 293 result = append(result, mt) 294 } 295 return result 296 } 297 298 func (e EngineState) TargetsBesides(mn model.ManifestName) []*ManifestTarget { 299 targets := e.Targets() 300 result := make([]*ManifestTarget, 0, len(targets)) 301 for _, mt := range targets { 302 if mt.Manifest.Name == mn { 303 continue 304 } 305 306 result = append(result, mt) 307 } 308 return result 309 } 310 311 func (e *EngineState) ManifestInTriggerQueue(mn model.ManifestName) bool { 312 for _, queued := range e.TriggerQueue { 313 if queued == mn { 314 return true 315 } 316 } 317 return false 318 } 319 320 func (e *EngineState) AppendToTriggerQueue(mn model.ManifestName, reason model.BuildReason) { 321 ms, ok := e.ManifestState(mn) 322 if !ok { 323 return 324 } 325 326 if reason == 0 { 327 reason = model.BuildReasonFlagTriggerUnknown 328 } 329 330 ms.TriggerReason = ms.TriggerReason.With(reason) 331 332 for _, queued := range e.TriggerQueue { 333 if mn == queued { 334 return 335 } 336 } 337 e.TriggerQueue = append(e.TriggerQueue, mn) 338 } 339 340 func (e *EngineState) RemoveFromTriggerQueue(mn model.ManifestName) { 341 mState, ok := e.ManifestState(mn) 342 if ok { 343 mState.TriggerReason = model.BuildReasonNone 344 } 345 346 for i, triggerName := range e.TriggerQueue { 347 if triggerName == mn { 348 e.TriggerQueue = append(e.TriggerQueue[:i], e.TriggerQueue[i+1:]...) 349 break 350 } 351 } 352 } 353 354 func (e EngineState) IsEmpty() bool { 355 return len(e.ManifestTargets) == 0 356 } 357 358 func (e EngineState) LastMainTiltfileError() error { 359 st, ok := e.TiltfileStates[model.MainTiltfileManifestName] 360 if !ok { 361 return nil 362 } 363 364 return st.LastBuild().Error 365 } 366 367 func (e *EngineState) MainTiltfileState() *ManifestState { 368 return e.TiltfileStates[model.MainTiltfileManifestName] 369 } 370 371 func (e *EngineState) HasBuild() bool { 372 for _, m := range e.Manifests() { 373 for _, targ := range m.ImageTargets { 374 if targ.IsDockerBuild() || targ.IsCustomBuild() { 375 return true 376 } 377 } 378 } 379 return false 380 } 381 382 func (e *EngineState) InitialBuildsCompleted() bool { 383 if len(e.ManifestTargets) == 0 { 384 return false 385 } 386 387 for _, mt := range e.ManifestTargets { 388 if !mt.Manifest.TriggerMode.AutoInitial() { 389 continue 390 } 391 392 ms, _ := e.ManifestState(mt.Manifest.Name) 393 if ms == nil || ms.LastBuild().Empty() { 394 return false 395 } 396 } 397 398 return true 399 } 400 401 type BuildStatus struct { 402 // We keep track of a change with two fields: 403 // 404 // 1) When we saw the change. 405 // 2) When we consumed the change. 406 // 407 // A change is considered pending if the last seen timestamp is later than the 408 // last consumed timestamp. 409 // 410 // Because these two fields are one-way ratchets (they only move forward in 411 // time), they avoid race conditions (e.g., if a we received Seen and Consumed 412 // events out of order). 413 ConsumedChanges time.Time 414 415 FileChanges map[string]time.Time 416 417 LastResult BuildResult 418 419 // Stores the times that dependencies were marked dirty, so we can prioritize 420 // the oldest one first. 421 // 422 // Long-term, we want to process all dependencies as a build graph rather than 423 // a list of manifests. Specifically, we'll build one Target at a time. Once 424 // the build completes, we'll look at all the targets that depend on it, and 425 // mark DependencyChanges to indicate that they need a rebuild. 426 // 427 // Short-term, we only use this for cases where two manifests share a common 428 // image. This only handles cross-manifest dependencies. 429 // 430 // This approach allows us to start working on the bookkeeping and 431 // dependency-tracking in the short-term, without having to switch over to a 432 // full dependency graph in one swoop. 433 DependencyChanges map[model.TargetID]time.Time 434 } 435 436 func newBuildStatus() *BuildStatus { 437 return &BuildStatus{ 438 FileChanges: make(map[string]time.Time), 439 DependencyChanges: make(map[model.TargetID]time.Time), 440 } 441 } 442 443 func (s *BuildStatus) ConsumeChangesBefore(startTime time.Time) { 444 if s.ConsumedChanges.IsZero() || s.ConsumedChanges.Before(startTime) { 445 s.ConsumedChanges = startTime 446 } 447 448 // Garbage collect changes consumed 449 // more than a minute ago. 450 for path, modTime := range s.FileChanges { 451 if modTime.Add(time.Minute).Before(s.ConsumedChanges) { 452 delete(s.FileChanges, path) 453 } 454 } 455 456 for targetID, modTime := range s.DependencyChanges { 457 if modTime.Add(time.Minute).Before(s.ConsumedChanges) { 458 delete(s.DependencyChanges, targetID) 459 } 460 } 461 } 462 463 func (s *BuildStatus) IsEmpty() bool { 464 return len(s.FileChanges) == 0 && 465 len(s.DependencyChanges) == 0 && 466 s.LastResult == nil 467 } 468 469 // Count PendingFileChanges 470 func (s *BuildStatus) CountPendingFileChanges() int { 471 count := 0 472 for _ = range s.PendingFileChanges() { 473 count++ 474 } 475 return count 476 } 477 478 func (s *BuildStatus) PendingFileChanges() iter.Seq2[string, time.Time] { 479 return func(yield func(string, time.Time) bool) { 480 neverConsumed := s.ConsumedChanges.IsZero() 481 for p, modTime := range s.FileChanges { 482 if neverConsumed || !timecmp.BeforeOrEqual(modTime, s.ConsumedChanges) { 483 if !yield(p, modTime) { 484 return 485 } 486 } 487 } 488 } 489 } 490 491 func (s *BuildStatus) PendingFileChangesList() []string { 492 var paths []string 493 for p, _ := range s.PendingFileChanges() { 494 paths = append(paths, p) 495 } 496 return paths 497 } 498 499 func (s *BuildStatus) PendingFileChangesSorted() []string { 500 result := s.PendingFileChangesList() 501 sort.Strings(result) 502 return result 503 } 504 505 func (s *BuildStatus) PendingDependencyChanges() iter.Seq2[model.TargetID, time.Time] { 506 return func(yield func(model.TargetID, time.Time) bool) { 507 neverConsumed := s.ConsumedChanges.IsZero() 508 for p, modTime := range s.DependencyChanges { 509 if neverConsumed || !timecmp.BeforeOrEqual(modTime, s.ConsumedChanges) { 510 if !yield(p, modTime) { 511 return 512 } 513 } 514 } 515 } 516 } 517 518 func (s *BuildStatus) HasPendingFileChanges() bool { 519 for _ = range s.PendingFileChanges() { 520 return true 521 } 522 return false 523 } 524 525 func (s *BuildStatus) HasPendingDependencyChanges() bool { 526 for _ = range s.PendingDependencyChanges() { 527 return true 528 } 529 return false 530 } 531 532 type ManifestState struct { 533 Name model.ManifestName 534 535 BuildStatuses map[model.TargetID]*BuildStatus 536 RuntimeState RuntimeState 537 538 PendingManifestChange time.Time 539 540 // Any current builds for this manifest. 541 // 542 // There can be multiple simultaneous image builds + deploys + live updates 543 // associated with a manifest. 544 // 545 // In an ideal world, we'd read these builds from the API models 546 // rather than do separate bookkeeping for them. 547 CurrentBuilds map[string]model.BuildRecord 548 549 LastSuccessfulDeployTime time.Time 550 551 // The last `BuildHistoryLimit` builds. The most recent build is first in the slice. 552 BuildHistory []model.BuildRecord 553 554 // If this manifest was changed, which config files led to the most recent change in manifest definition 555 ConfigFilesThatCausedChange []string 556 557 // If the build was manually triggered, record why. 558 TriggerReason model.BuildReason 559 560 DisableState v1alpha1.DisableState 561 } 562 563 func NewState() *EngineState { 564 ret := &EngineState{} 565 ret.LogStore = logstore.NewLogStore() 566 ret.ManifestTargets = make(map[model.ManifestName]*ManifestTarget) 567 ret.Secrets = model.SecretSet{} 568 ret.DockerPruneSettings = model.DefaultDockerPruneSettings() 569 ret.VersionSettings = model.VersionSettings{ 570 CheckUpdates: true, 571 } 572 ret.UpdateSettings = model.DefaultUpdateSettings() 573 ret.CurrentBuildSet = make(map[model.ManifestName]bool) 574 575 // For most Tiltfiles, this is created by the TiltfileUpsertAction. But 576 // lots of tests assume tha main tiltfile state exists on initialization. 577 ret.TiltfileDefinitionOrder = []model.ManifestName{model.MainTiltfileManifestName} 578 ret.TiltfileStates = map[model.ManifestName]*ManifestState{ 579 model.MainTiltfileManifestName: NewTiltfileManifestState(model.MainTiltfileManifestName), 580 } 581 582 if ok, _ := tiltanalytics.IsAnalyticsDisabledFromEnv(); ok { 583 ret.AnalyticsEnvOpt = analytics.OptOut 584 } 585 586 ret.Cmds = make(map[string]*Cmd) 587 ret.Tiltfiles = make(map[string]*v1alpha1.Tiltfile) 588 ret.FileWatches = make(map[string]*v1alpha1.FileWatch) 589 ret.KubernetesApplys = make(map[string]*v1alpha1.KubernetesApply) 590 ret.DockerComposeServices = make(map[string]*v1alpha1.DockerComposeService) 591 ret.KubernetesDiscoverys = make(map[string]*v1alpha1.KubernetesDiscovery) 592 ret.KubernetesResources = make(map[string]*k8sconv.KubernetesResource) 593 ret.UIResources = make(map[string]*v1alpha1.UIResource) 594 ret.ConfigMaps = make(map[string]*v1alpha1.ConfigMap) 595 ret.LiveUpdates = make(map[string]*v1alpha1.LiveUpdate) 596 ret.Clusters = make(map[string]*v1alpha1.Cluster) 597 ret.UIButtons = make(map[string]*v1alpha1.UIButton) 598 ret.ImageMaps = make(map[string]*v1alpha1.ImageMap) 599 ret.DockerImages = make(map[string]*v1alpha1.DockerImage) 600 ret.CmdImages = make(map[string]*v1alpha1.CmdImage) 601 602 return ret 603 } 604 605 func NewTiltfileManifestState(n model.ManifestName) *ManifestState { 606 ms := &ManifestState{ 607 Name: n, 608 DisableState: v1alpha1.DisableStateEnabled, 609 CurrentBuilds: make(map[string]model.BuildRecord), 610 } 611 ms.ResetTiltfileBuildStatus(n) 612 return ms 613 } 614 615 func NewManifestState(m model.Manifest) *ManifestState { 616 mn := m.Name 617 ms := &ManifestState{ 618 Name: mn, 619 DisableState: v1alpha1.DisableStatePending, 620 CurrentBuilds: make(map[string]model.BuildRecord), 621 } 622 623 ms.ResetBuildStatus(m) 624 625 if m.IsK8s() { 626 ms.RuntimeState = NewK8sRuntimeState(m) 627 } else if m.IsLocal() { 628 ms.RuntimeState = LocalRuntimeState{} 629 } 630 631 // For historical reasons, DC state is initialized differently. 632 633 return ms 634 } 635 636 func (ms *ManifestState) ResetTiltfileBuildStatus(n model.ManifestName) { 637 ms.BuildStatuses = make(map[model.TargetID]*BuildStatus) 638 ms.BuildStatuses[model.TargetID{ 639 Type: model.TargetTypeConfigs, 640 Name: model.TargetName(n), 641 }] = newBuildStatus() 642 } 643 644 func (ms *ManifestState) ResetBuildStatus(m model.Manifest) { 645 ms.BuildStatuses = make(map[model.TargetID]*BuildStatus) 646 for _, spec := range m.TargetSpecs() { 647 ms.BuildStatuses[spec.ID()] = newBuildStatus() 648 } 649 } 650 651 func (ms *ManifestState) TargetID() model.TargetID { 652 return ms.Name.TargetID() 653 } 654 655 // Returns the build status. 656 func (ms *ManifestState) BuildStatus(id model.TargetID) (*BuildStatus, bool) { 657 result, ok := ms.BuildStatuses[id] 658 return result, ok 659 } 660 661 func (ms *ManifestState) DCRuntimeState() dockercompose.State { 662 ret, _ := ms.RuntimeState.(dockercompose.State) 663 return ret 664 } 665 666 func (ms *ManifestState) IsDC() bool { 667 _, ok := ms.RuntimeState.(dockercompose.State) 668 return ok 669 } 670 671 func (ms *ManifestState) K8sRuntimeState() K8sRuntimeState { 672 ret, _ := ms.RuntimeState.(K8sRuntimeState) 673 return ret 674 } 675 676 func (ms *ManifestState) IsK8s() bool { 677 _, ok := ms.RuntimeState.(K8sRuntimeState) 678 return ok 679 } 680 681 func (ms *ManifestState) LocalRuntimeState() LocalRuntimeState { 682 ret, _ := ms.RuntimeState.(LocalRuntimeState) 683 return ret 684 } 685 686 // Return the current build that started first. 687 func (ms *ManifestState) EarliestCurrentBuild() model.BuildRecord { 688 best := model.BuildRecord{} 689 bestKey := "" 690 for k, v := range ms.CurrentBuilds { 691 if best.StartTime.IsZero() || best.StartTime.After(v.StartTime) || (best.StartTime == v.StartTime && k < bestKey) { 692 best = v 693 bestKey = k 694 } 695 } 696 return best 697 } 698 699 func (ms *ManifestState) IsBuilding() bool { 700 return len(ms.CurrentBuilds) != 0 701 } 702 703 func (ms *ManifestState) LastBuild() model.BuildRecord { 704 if len(ms.BuildHistory) == 0 { 705 return model.BuildRecord{} 706 } 707 return ms.BuildHistory[0] 708 } 709 710 func (ms *ManifestState) AddCompletedBuild(bs model.BuildRecord) { 711 ms.BuildHistory = append([]model.BuildRecord{bs}, ms.BuildHistory...) 712 if len(ms.BuildHistory) > model.BuildHistoryLimit { 713 ms.BuildHistory = ms.BuildHistory[:model.BuildHistoryLimit] 714 } 715 } 716 717 func (ms *ManifestState) StartedFirstBuild() bool { 718 return ms.IsBuilding() || len(ms.BuildHistory) > 0 719 } 720 721 func (ms *ManifestState) MostRecentPod() v1alpha1.Pod { 722 return ms.K8sRuntimeState().MostRecentPod() 723 } 724 725 func (ms *ManifestState) PodWithID(pid k8s.PodID) (*v1alpha1.Pod, bool) { 726 name := string(pid) 727 for _, pod := range ms.K8sRuntimeState().GetPods() { 728 if name == pod.Name { 729 return &pod, true 730 } 731 } 732 return nil, false 733 } 734 735 func (ms *ManifestState) AddPendingFileChange(targetID model.TargetID, file string, timestamp time.Time) { 736 if ms.IsBuilding() { 737 build := ms.EarliestCurrentBuild() 738 if timestamp.Before(build.StartTime) { 739 // this file change occurred before the build started, but if the current build already knows 740 // about it (from another target or rapid successive changes that weren't de-duped), it can be ignored 741 for _, edit := range build.Edits { 742 if edit == file { 743 return 744 } 745 } 746 } 747 // NOTE(nick): BuildController uses these timestamps to determine which files 748 // to clear after a build. In particular, it: 749 // 750 // 1) Grabs the pending files 751 // 2) Runs a live update 752 // 3) Clears the pending files with timestamps before the live update started. 753 // 754 // Here's the race condition: suppose a file changes, but it doesn't get into 755 // the EngineState until after step (2). That means step (3) will clear the file 756 // even though it wasn't live-updated properly. Because as far as we can tell, 757 // the file must have been in the EngineState before the build started. 758 // 759 // Ideally, BuildController should be do more bookkeeping to keep track of 760 // which files it consumed from which FileWatches. But we're changing 761 // this architecture anyway. For now, we record the time it got into 762 // the EngineState, rather than the time it was originally changed. 763 // 764 // This will all go away as we move things into reconcilers, 765 // because reconcilers do synchronous state updates. 766 isReconciler := targetID.Type == model.TargetTypeConfigs 767 if !isReconciler { 768 timestamp = time.Now() 769 } 770 } 771 772 bs, ok := ms.BuildStatus(targetID) 773 if ok { 774 bs.FileChanges[file] = timestamp 775 } 776 } 777 778 func (ms *ManifestState) HasPendingFileChanges() bool { 779 for _, status := range ms.BuildStatuses { 780 if status.HasPendingFileChanges() { 781 return true 782 } 783 } 784 return false 785 } 786 787 func (ms *ManifestState) HasPendingDependencyChanges() bool { 788 for _, status := range ms.BuildStatuses { 789 if status.HasPendingDependencyChanges() { 790 return true 791 } 792 } 793 return false 794 } 795 796 func (mt *ManifestTarget) NextBuildReason() model.BuildReason { 797 state := mt.State 798 reason := state.TriggerReason 799 if mt.State.HasPendingFileChanges() { 800 reason = reason.With(model.BuildReasonFlagChangedFiles) 801 } 802 if mt.State.HasPendingDependencyChanges() { 803 reason = reason.With(model.BuildReasonFlagChangedDeps) 804 } 805 if !mt.State.PendingManifestChange.IsZero() { 806 reason = reason.With(model.BuildReasonFlagConfig) 807 } 808 if !mt.State.StartedFirstBuild() && mt.Manifest.TriggerMode.AutoInitial() { 809 reason = reason.With(model.BuildReasonFlagInit) 810 } 811 return reason 812 } 813 814 // Whether changes have been made to this Manifest's synced files 815 // or config since the last build. 816 // 817 // Returns: 818 // bool: whether changes have been made 819 // Time: the time of the earliest change 820 func (ms *ManifestState) HasPendingChanges() (bool, time.Time) { 821 return ms.HasPendingChangesBeforeOrEqual(time.Now()) 822 } 823 824 // Like HasPendingChanges, but relative to a particular time. 825 func (ms *ManifestState) HasPendingChangesBeforeOrEqual(highWaterMark time.Time) (bool, time.Time) { 826 ok := false 827 earliest := highWaterMark 828 t := ms.PendingManifestChange 829 if !t.IsZero() && timecmp.BeforeOrEqual(t, earliest) { 830 ok = true 831 earliest = t 832 } 833 834 for _, status := range ms.BuildStatuses { 835 for _, t := range status.PendingFileChanges() { 836 if !t.IsZero() && timecmp.BeforeOrEqual(t, earliest) { 837 ok = true 838 earliest = t 839 } 840 } 841 842 for _, t := range status.PendingDependencyChanges() { 843 if !t.IsZero() && timecmp.BeforeOrEqual(t, earliest) { 844 ok = true 845 earliest = t 846 } 847 } 848 } 849 if !ok { 850 return ok, time.Time{} 851 } 852 return ok, earliest 853 } 854 855 func (ms *ManifestState) UpdateStatus(triggerMode model.TriggerMode) v1alpha1.UpdateStatus { 856 currentBuild := ms.EarliestCurrentBuild() 857 hasPendingChanges, _ := ms.HasPendingChanges() 858 lastBuild := ms.LastBuild() 859 lastBuildError := lastBuild.Error != nil 860 hasPendingBuild := false 861 if ms.TriggerReason != 0 { 862 hasPendingBuild = true 863 } else if triggerMode.AutoOnChange() && hasPendingChanges { 864 hasPendingBuild = true 865 } else if triggerMode.AutoInitial() && currentBuild.Empty() && lastBuild.Empty() { 866 hasPendingBuild = true 867 } 868 869 if !currentBuild.Empty() { 870 return v1alpha1.UpdateStatusInProgress 871 } else if hasPendingBuild { 872 return v1alpha1.UpdateStatusPending 873 } else if lastBuildError { 874 return v1alpha1.UpdateStatusError 875 } else if !lastBuild.Empty() { 876 return v1alpha1.UpdateStatusOK 877 } 878 return v1alpha1.UpdateStatusNone 879 } 880 881 // Check the runtime status of the individual status fields. 882 // 883 // The individual status fields don't know anything about how resources are 884 // triggered (i.e., whether they're waiting on a dependent resource to build or 885 // a manual trigger). So we need to consider that information here. 886 func (ms *ManifestState) RuntimeStatus(triggerMode model.TriggerMode) v1alpha1.RuntimeStatus { 887 runStatus := v1alpha1.RuntimeStatusUnknown 888 if ms.RuntimeState != nil { 889 runStatus = ms.RuntimeState.RuntimeStatus() 890 } 891 892 if runStatus == v1alpha1.RuntimeStatusPending || runStatus == v1alpha1.RuntimeStatusUnknown { 893 // Let's just borrow the trigger analysis logic from UpdateStatus(). 894 updateStatus := ms.UpdateStatus(triggerMode) 895 if updateStatus == v1alpha1.UpdateStatusNone { 896 runStatus = v1alpha1.RuntimeStatusNone 897 } else if updateStatus == v1alpha1.UpdateStatusPending || updateStatus == v1alpha1.UpdateStatusInProgress { 898 runStatus = v1alpha1.RuntimeStatusPending 899 } 900 } 901 return runStatus 902 } 903 904 var _ model.TargetStatus = &ManifestState{} 905 906 func ManifestTargetEndpoints(mt *ManifestTarget) (endpoints []model.Link) { 907 if mt.Manifest.IsK8s() { 908 k8sTarg := mt.Manifest.K8sTarget() 909 endpoints = append(endpoints, k8sTarg.Links...) 910 911 // If the user specified port-forwards in the Tiltfile, we 912 // assume that's what they want to see in the UI (so it 913 // takes precedence over any load balancer URLs 914 portForwardSpec := k8sTarg.PortForwardTemplateSpec 915 if portForwardSpec != nil && len(portForwardSpec.Forwards) > 0 { 916 for _, pf := range portForwardSpec.Forwards { 917 endpoints = append(endpoints, model.PortForwardToLink(pf)) 918 } 919 return endpoints 920 } 921 922 lbEndpoints := []model.Link{} 923 for _, u := range mt.State.K8sRuntimeState().LBs { 924 if u != nil { 925 lbEndpoints = append(lbEndpoints, model.Link{URL: u}) 926 } 927 } 928 // Sort so the ordering of LB endpoints is deterministic 929 // (otherwise it's not, because they live in a map) 930 sort.Sort(model.ByURL(lbEndpoints)) 931 endpoints = append(endpoints, lbEndpoints...) 932 } 933 934 localResourceLinks := mt.Manifest.LocalTarget().Links 935 if len(localResourceLinks) > 0 { 936 return localResourceLinks 937 } 938 939 if mt.Manifest.IsDC() { 940 hostPorts := make(map[int32]bool) 941 publishedPorts := mt.Manifest.DockerComposeTarget().PublishedPorts() 942 inferLinks := mt.Manifest.DockerComposeTarget().InferLinks() 943 for _, p := range publishedPorts { 944 if p == 0 || hostPorts[int32(p)] { 945 continue 946 } 947 hostPorts[int32(p)] = true 948 if inferLinks { 949 endpoints = append(endpoints, model.MustNewLink(fmt.Sprintf("http://localhost:%d/", p), "")) 950 } 951 } 952 953 for _, binding := range mt.State.DCRuntimeState().Ports { 954 // Docker usually contains multiple bindings for each port - one for ipv4 (0.0.0.0) 955 // and one for ipv6 (::1). 956 p := binding.HostPort 957 if hostPorts[p] { 958 continue 959 } 960 hostPorts[p] = true 961 if inferLinks { 962 endpoints = append(endpoints, model.MustNewLink(fmt.Sprintf("http://localhost:%d/", p), "")) 963 } 964 } 965 966 endpoints = append(endpoints, mt.Manifest.DockerComposeTarget().Links...) 967 } 968 969 return endpoints 970 } 971 972 const MainTiltfileManifestName = model.MainTiltfileManifestName