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