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  }