github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/engine/k8srollout/podmonitor.go (about)

     1  package k8srollout
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  	"time"
     8  
     9  	"github.com/google/go-cmp/cmp"
    10  	"github.com/jonboulle/clockwork"
    11  	v1 "k8s.io/api/core/v1"
    12  
    13  	"github.com/tilt-dev/tilt/internal/k8s"
    14  	"github.com/tilt-dev/tilt/internal/store"
    15  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    16  	"github.com/tilt-dev/tilt/pkg/logger"
    17  	"github.com/tilt-dev/tilt/pkg/model"
    18  	"github.com/tilt-dev/tilt/pkg/model/logstore"
    19  )
    20  
    21  type podManifest struct {
    22  	pod      k8s.PodID
    23  	manifest model.ManifestName
    24  }
    25  
    26  type PodMonitor struct {
    27  	pods            map[podManifest]podStatus
    28  	trackingStarted map[podManifest]bool
    29  	startTime       time.Time
    30  }
    31  
    32  func NewPodMonitor(clock clockwork.Clock) *PodMonitor {
    33  	return &PodMonitor{
    34  		pods:            make(map[podManifest]podStatus),
    35  		trackingStarted: make(map[podManifest]bool),
    36  		startTime:       clock.Now(),
    37  	}
    38  }
    39  
    40  func (m *PodMonitor) diff(st store.RStore) []podStatus {
    41  	state := st.RLockState()
    42  	defer st.RUnlockState()
    43  
    44  	updates := make([]podStatus, 0)
    45  	active := make(map[podManifest]bool)
    46  
    47  	for _, mt := range state.Targets() {
    48  		ms := mt.State
    49  		manifest := mt.Manifest
    50  		pod := ms.MostRecentPod()
    51  		podID := k8s.PodID(pod.Name)
    52  		if podID.Empty() {
    53  			continue
    54  		}
    55  
    56  		key := podManifest{pod: podID, manifest: manifest.Name}
    57  		active[key] = true
    58  
    59  		currentStatus := newPodStatus(pod, manifest.Name)
    60  		if !podStatusesEqual(currentStatus, m.pods[key]) {
    61  			updates = append(updates, currentStatus)
    62  			m.pods[key] = currentStatus
    63  		}
    64  	}
    65  
    66  	for key := range m.pods {
    67  		if !active[key] {
    68  			delete(m.pods, key)
    69  		}
    70  	}
    71  
    72  	return updates
    73  }
    74  
    75  func (m *PodMonitor) OnChange(ctx context.Context, st store.RStore, _ store.ChangeSummary) error {
    76  	updates := m.diff(st)
    77  	for _, update := range updates {
    78  		ctx := store.WithManifestLogHandler(ctx, st, update.manifestName, spanIDForPod(update.manifestName, update.podID))
    79  		m.print(ctx, update)
    80  	}
    81  
    82  	return nil
    83  }
    84  
    85  func (m *PodMonitor) print(ctx context.Context, update podStatus) {
    86  	key := podManifest{pod: update.podID, manifest: update.manifestName}
    87  
    88  	if !m.trackingStarted[key] {
    89  		m.trackingStarted[key] = true
    90  
    91  		if m.startTime.After(update.startTime) {
    92  			logger.Get(ctx).Infof("\nAttaching to existing pod (%s). Only new logs will be streamed.", update.podID)
    93  			return
    94  		}
    95  		logger.Get(ctx).Infof("\nTracking new pod rollout (%s):", update.podID)
    96  	}
    97  
    98  	m.printCondition(ctx, "Scheduled", update.scheduled, update.startTime)
    99  	m.printCondition(ctx, "Initialized", update.initialized, update.scheduled.LastTransitionTime.Time)
   100  	m.printCondition(ctx, "Ready", update.ready, update.initialized.LastTransitionTime.Time)
   101  }
   102  
   103  func (m *PodMonitor) printCondition(ctx context.Context, name string, cond v1alpha1.PodCondition, startTime time.Time) {
   104  	l := logger.Get(ctx).WithFields(logger.Fields{logger.FieldNameProgressID: name})
   105  
   106  	indent := "     "
   107  	duration := ""
   108  	spacerMax := 16
   109  	spacer := ""
   110  	if len(name) > spacerMax {
   111  		name = name[:spacerMax-1] + "…"
   112  	} else {
   113  		spacer = strings.Repeat(" ", spacerMax-len(name))
   114  	}
   115  
   116  	dur := cond.LastTransitionTime.Sub(startTime)
   117  	if !startTime.IsZero() && !cond.LastTransitionTime.IsZero() {
   118  		if dur == 0 {
   119  			duration = "<1s"
   120  		} else {
   121  			duration = fmt.Sprint(dur.Truncate(time.Millisecond))
   122  		}
   123  	}
   124  
   125  	if cond.Status == string(v1.ConditionTrue) {
   126  		l.Infof("%s┊ %s%s- %s", indent, name, spacer, duration)
   127  		return
   128  	}
   129  
   130  	// PodConditions unfortunately don't represent Jobs well:
   131  	// 1) If a Job runs quickly enough, we might never observe the pod in a ready state (i.e., no updates have Type="Ready" and Status="True")
   132  	// 2) After a Job finishes, we get a pod update with Type="Ready", Status="False", and LastTransitionTime=the job end time,
   133  	//    meaning that we lose the time at which the pod actually transitioned to the ready state.
   134  	// Rather than invest in more state to track these for the Job case, let's just replace "Ready" with "Completed"
   135  	// and reconsider if/when a user cares.
   136  	if cond.Type == "Ready" && cond.Reason == "PodCompleted" {
   137  		name = "Completed"
   138  		spacer = strings.Repeat(" ", spacerMax-len(name))
   139  		l.Infof("%s┊ %s%s- %s", indent, name, spacer, duration)
   140  		return
   141  	}
   142  
   143  	message := cond.Message
   144  	reason := cond.Reason
   145  	if cond.Status == "" || reason == "" || message == "" {
   146  		l.Infof("%s┊ %s%s- (…) Pending", indent, name, spacer)
   147  		return
   148  	}
   149  
   150  	prefix := "Not "
   151  	spacer = strings.Repeat(" ", spacerMax-len(name)-len(prefix))
   152  	l.Infof("%s┃ %s%s%s- (%s): %s", indent, prefix, name, spacer, reason, message)
   153  }
   154  
   155  type podStatus struct {
   156  	podID        k8s.PodID
   157  	manifestName model.ManifestName
   158  	startTime    time.Time
   159  	scheduled    v1alpha1.PodCondition
   160  	initialized  v1alpha1.PodCondition
   161  	ready        v1alpha1.PodCondition
   162  }
   163  
   164  func newPodStatus(pod v1alpha1.Pod, manifestName model.ManifestName) podStatus {
   165  	s := podStatus{podID: k8s.PodID(pod.Name), manifestName: manifestName, startTime: pod.CreatedAt.Time}
   166  	for _, condition := range pod.Conditions {
   167  		switch v1.PodConditionType(condition.Type) {
   168  		case v1.PodScheduled:
   169  			s.scheduled = condition
   170  		case v1.PodInitialized:
   171  			s.initialized = condition
   172  		case v1.PodReady:
   173  			s.ready = condition
   174  		}
   175  	}
   176  	return s
   177  }
   178  
   179  var podStatusAllowUnexported = cmp.AllowUnexported(podStatus{})
   180  
   181  func podStatusesEqual(a, b podStatus) bool {
   182  	return cmp.Equal(a, b, podStatusAllowUnexported)
   183  }
   184  
   185  func spanIDForPod(mn model.ManifestName, podID k8s.PodID) logstore.SpanID {
   186  	return logstore.SpanID(fmt.Sprintf("monitor:%s:%s", mn, podID))
   187  }
   188  
   189  var _ store.Subscriber = &PodMonitor{}