github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/controllers/core/session/status.go (about) 1 package session 2 3 import ( 4 "fmt" 5 "sort" 6 "strings" 7 8 ctrl "sigs.k8s.io/controller-runtime" 9 10 "github.com/tilt-dev/tilt/internal/engine/buildcontrol" 11 "github.com/tilt-dev/tilt/internal/store" 12 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 13 "github.com/tilt-dev/tilt/pkg/model" 14 ) 15 16 func (r *Reconciler) makeLatestStatus(session *v1alpha1.Session, result *ctrl.Result) v1alpha1.SessionStatus { 17 state := r.st.RLockState() 18 defer r.st.RUnlockState() 19 20 status := v1alpha1.SessionStatus{ 21 PID: session.Status.PID, 22 StartTime: session.Status.StartTime, 23 } 24 25 // A session only captures services that are created by the main Tiltfile 26 // entrypoint. We don't consider any extension Tiltfiles or Manifests created 27 // by them. 28 ms, ok := state.TiltfileStates[model.MainTiltfileManifestName] 29 if ok { 30 status.Targets = append(status.Targets, tiltfileTarget(model.MainTiltfileManifestName, ms)) 31 } 32 33 // determine the reason any resources (and thus all of their targets) are waiting (aka "holds") 34 // N.B. we don't actually care about what's "next" to build, but the info comes alongside that 35 _, holds := buildcontrol.NextTargetToBuild(state) 36 37 for _, mt := range state.ManifestTargets { 38 status.Targets = append(status.Targets, r.targetsForResource(mt, holds, session.Spec.CI, result)...) 39 } 40 // ensure consistent ordering to avoid unnecessary updates 41 sort.SliceStable(status.Targets, func(i, j int) bool { 42 return status.Targets[i].Name < status.Targets[j].Name 43 }) 44 45 r.processExitCondition(session.Spec, &state, &status) 46 47 // If there's a global timeout, schedule a requeue. 48 ci := session.Spec.CI 49 if ci != nil && ci.Timeout != nil && ci.Timeout.Duration > 0 { 50 timeout := ci.Timeout.Duration 51 requeueAfter := timeout - r.clock.Since(session.Status.StartTime.Time) 52 if result.RequeueAfter == 0 || result.RequeueAfter > requeueAfter { 53 result.RequeueAfter = requeueAfter 54 } 55 } 56 57 return status 58 } 59 60 func (r *Reconciler) processExitCondition(spec v1alpha1.SessionSpec, state *store.EngineState, status *v1alpha1.SessionStatus) { 61 exitCondition := spec.ExitCondition 62 if exitCondition == v1alpha1.ExitConditionManual { 63 return 64 } else if exitCondition != v1alpha1.ExitConditionCI { 65 status.Done = true 66 status.Error = fmt.Sprintf("unsupported exit condition: %s", exitCondition) 67 } 68 69 var waiting []string 70 var notReady []string 71 var retrying []string 72 73 allResourcesOK := func() bool { 74 return len(waiting)+len(notReady)+len(retrying) == 0 75 } 76 77 for _, res := range status.Targets { 78 if res.State.Waiting == nil && res.State.Active == nil && res.State.Terminated == nil { 79 // if all states are nil, the target has not been requested to run, e.g. auto_init=False 80 continue 81 } 82 83 isTerminated := res.State.Terminated != nil && res.State.Terminated.Error != "" 84 if isTerminated { 85 if res.State.Terminated.GraceStatus == v1alpha1.TargetGraceTolerated { 86 retrying = append(retrying, res.Name) 87 continue 88 } 89 90 err := res.State.Terminated.Error 91 if res.State.Terminated.GraceStatus == v1alpha1.TargetGraceExceeded { 92 err = fmt.Sprintf("exceeded grace period: %v", err) 93 } 94 95 status.Done = true 96 status.Error = err 97 return 98 } 99 if res.State.Waiting != nil { 100 waiting = append(waiting, fmt.Sprintf("%v %v", res.Name, res.State.Waiting.WaitReason)) 101 } else if res.State.Active != nil && !res.State.Active.Ready { 102 // jobs must run to completion 103 notReady = append(notReady, res.Name) 104 } 105 } 106 107 // Tiltfile is _always_ a target, so ensure that there's at least one other real target, or it's possible to 108 // exit before the targets have actually been initialized 109 if allResourcesOK() && len(status.Targets) > 1 { 110 status.Done = true 111 } 112 113 summary := func() string { 114 buf := new(strings.Builder) 115 for _, category := range []struct { 116 name string 117 items []string 118 }{ 119 {name: "waiting", items: waiting}, 120 {name: "not ready", items: notReady}, 121 {name: "retrying", items: retrying}, 122 } { 123 if num := len(category.items); num > 0 { 124 if buf.Len() > 0 { 125 buf.WriteString(", ") 126 } 127 fmt.Fprintf(buf, "%d resources %v (%v)", 128 num, category.name, strings.Join(category.items, ",")) 129 } 130 } 131 return buf.String() 132 } 133 134 // Enforce a global timeout. 135 ci := spec.CI 136 if status.Error == "" && ci != nil && ci.Timeout != nil && ci.Timeout.Duration > 0 && 137 r.clock.Since(status.StartTime.Time) > ci.Timeout.Duration { 138 status.Done = true 139 status.Error = fmt.Sprintf("Timeout after %s: %v", ci.Timeout.Duration, summary()) 140 } 141 } 142 143 // errToString returns a stringified version of an error or an empty string if the error is nil. 144 func errToString(err error) string { 145 if err == nil { 146 return "" 147 } 148 return err.Error() 149 }