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