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  }