github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/store/runtime_state.go (about)

     1  package store
     2  
     3  import (
     4  	"fmt"
     5  	"net/url"
     6  	"time"
     7  
     8  	"k8s.io/apimachinery/pkg/api/meta"
     9  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    10  	"k8s.io/apimachinery/pkg/runtime/schema"
    11  
    12  	"github.com/tilt-dev/tilt/internal/store/k8sconv"
    13  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    14  
    15  	v1 "k8s.io/api/core/v1"
    16  
    17  	"github.com/tilt-dev/tilt/internal/k8s"
    18  	"github.com/tilt-dev/tilt/pkg/model"
    19  )
    20  
    21  type RuntimeState interface {
    22  	RuntimeState()
    23  
    24  	// There are two types of resource dependencies:
    25  	// - servers (Deployments) where what's important is that the server is running
    26  	// - tasks (Jobs, local resources) where what's important is that the job completed
    27  	// Currently, we don't try to distinguish between these two cases.
    28  	//
    29  	// In the future, it might make sense to check "IsBlocking()" or something,
    30  	// and alter the behavior based on whether the underlying resource is a server
    31  	// or a task.
    32  	HasEverBeenReadyOrSucceeded() bool
    33  
    34  	RuntimeStatus() v1alpha1.RuntimeStatus
    35  
    36  	// If the runtime status is in Error mode,
    37  	// RuntimeStatusError() should report a reason.
    38  	RuntimeStatusError() error
    39  }
    40  
    41  type LocalRuntimeState struct {
    42  	CmdName                  string
    43  	Status                   v1alpha1.RuntimeStatus
    44  	PID                      int
    45  	StartTime                time.Time
    46  	FinishTime               time.Time
    47  	SpanID                   model.LogSpanID
    48  	LastReadyOrSucceededTime time.Time
    49  	Ready                    bool
    50  }
    51  
    52  var _ RuntimeState = LocalRuntimeState{}
    53  
    54  func (LocalRuntimeState) RuntimeState() {}
    55  
    56  func (l LocalRuntimeState) RuntimeStatus() v1alpha1.RuntimeStatus {
    57  	status := l.Status
    58  	if status == "" {
    59  		status = v1alpha1.RuntimeStatusUnknown
    60  	}
    61  	return status
    62  }
    63  
    64  func (l LocalRuntimeState) RuntimeStatusError() error {
    65  	status := l.RuntimeStatus()
    66  	if status != v1alpha1.RuntimeStatusError {
    67  		return nil
    68  	}
    69  	return fmt.Errorf("Process %d exited with non-zero status", l.PID)
    70  }
    71  
    72  func (l LocalRuntimeState) HasEverBeenReadyOrSucceeded() bool {
    73  	return !l.LastReadyOrSucceededTime.IsZero()
    74  }
    75  
    76  type K8sRuntimeState struct {
    77  	LBs map[k8s.ServiceName]*url.URL
    78  
    79  	ApplyFilter *k8sconv.KubernetesApplyFilter
    80  
    81  	// This must match the FilteredPods field of k8sconv.KubernetesResource
    82  	FilteredPods []v1alpha1.Pod
    83  
    84  	// Conditions from the apply operation; must match the Conditions field
    85  	// from k8sconv.KubernetesResource::ApplyStatus.
    86  	Conditions []metav1.Condition
    87  
    88  	LastReadyOrSucceededTime    time.Time
    89  	HasEverDeployedSuccessfully bool
    90  
    91  	UpdateStartTime map[k8s.PodID]time.Time
    92  
    93  	PodReadinessMode model.PodReadinessMode
    94  }
    95  
    96  func (K8sRuntimeState) RuntimeState() {}
    97  
    98  var _ RuntimeState = K8sRuntimeState{}
    99  
   100  func NewK8sRuntimeStateWithPods(m model.Manifest, pods ...v1alpha1.Pod) K8sRuntimeState {
   101  	state := NewK8sRuntimeState(m)
   102  	state.FilteredPods = pods
   103  	state.HasEverDeployedSuccessfully = len(pods) > 0
   104  	return state
   105  }
   106  
   107  func NewK8sRuntimeState(m model.Manifest) K8sRuntimeState {
   108  	return K8sRuntimeState{
   109  		PodReadinessMode: m.PodReadinessMode(),
   110  		LBs:              make(map[k8s.ServiceName]*url.URL),
   111  		UpdateStartTime:  make(map[k8s.PodID]time.Time),
   112  	}
   113  }
   114  
   115  func (s K8sRuntimeState) RuntimeStatusError() error {
   116  	status := s.RuntimeStatus()
   117  	if status != v1alpha1.RuntimeStatusError {
   118  		return nil
   119  	}
   120  	pod := s.MostRecentPod()
   121  	return fmt.Errorf("Pod %s in error state: %s", pod.Name, pod.Status)
   122  }
   123  
   124  func (s K8sRuntimeState) RuntimeStatus() v1alpha1.RuntimeStatus {
   125  	if !s.HasEverDeployedSuccessfully {
   126  		return v1alpha1.RuntimeStatusPending
   127  	}
   128  
   129  	if s.PodReadinessMode == model.PodReadinessIgnore {
   130  		return v1alpha1.RuntimeStatusOK
   131  	}
   132  
   133  	// if the apply indicated that the Job had already completed, we can skip
   134  	// inspecting the Pods, which avoids issues in the event that the Job's
   135  	// Pod was GC'd
   136  	if meta.IsStatusConditionTrue(s.Conditions, v1alpha1.ApplyConditionJobComplete) {
   137  		return v1alpha1.RuntimeStatusOK
   138  	}
   139  
   140  	pod := s.MostRecentPod()
   141  	switch v1.PodPhase(pod.Phase) {
   142  	case v1.PodRunning:
   143  		if AllPodContainersReady(pod) && s.PodReadinessMode != model.PodReadinessSucceeded {
   144  			return v1alpha1.RuntimeStatusOK
   145  		}
   146  		return v1alpha1.RuntimeStatusPending
   147  
   148  	case v1.PodSucceeded:
   149  		return v1alpha1.RuntimeStatusOK
   150  
   151  	case v1.PodFailed:
   152  		return v1alpha1.RuntimeStatusError
   153  	}
   154  
   155  	for _, c := range AllPodContainers(pod) {
   156  		if k8sconv.ContainerStatusToRuntimeState(c) == v1alpha1.RuntimeStatusError {
   157  			return v1alpha1.RuntimeStatusError
   158  		}
   159  	}
   160  
   161  	return v1alpha1.RuntimeStatusPending
   162  }
   163  
   164  func (s K8sRuntimeState) HasEverBeenReadyOrSucceeded() bool {
   165  	if !s.HasEverDeployedSuccessfully {
   166  		return false
   167  	}
   168  	if s.PodReadinessMode == model.PodReadinessIgnore {
   169  		return true
   170  	}
   171  	return !s.LastReadyOrSucceededTime.IsZero()
   172  }
   173  
   174  func (s K8sRuntimeState) PodLen() int {
   175  	return len(s.FilteredPods)
   176  }
   177  
   178  func (s K8sRuntimeState) ContainsID(id k8s.PodID) bool {
   179  	name := string(id)
   180  	for _, pod := range s.FilteredPods {
   181  		if pod.Name == name {
   182  			return true
   183  		}
   184  	}
   185  	return false
   186  }
   187  
   188  func (s K8sRuntimeState) GetPods() []v1alpha1.Pod {
   189  	return s.FilteredPods
   190  }
   191  
   192  func (s K8sRuntimeState) EntityDisplayNames() []string {
   193  	if s.ApplyFilter == nil {
   194  		return nil
   195  	}
   196  
   197  	entities := make([]k8s.EntityMeta, len(s.ApplyFilter.DeployedRefs))
   198  	for i := range s.ApplyFilter.DeployedRefs {
   199  		entities[i] = objectRefMeta{s.ApplyFilter.DeployedRefs[i]}
   200  	}
   201  
   202  	// Use a min component count of 2 for computing names,
   203  	// so that the resource type appears
   204  	return k8s.UniqueNamesMeta(entities, 2)
   205  }
   206  
   207  type objectRefMeta struct {
   208  	v1.ObjectReference
   209  }
   210  
   211  func (o objectRefMeta) Name() string {
   212  	return o.ObjectReference.Name
   213  }
   214  
   215  func (o objectRefMeta) Namespace() k8s.Namespace {
   216  	return k8s.Namespace(o.ObjectReference.Namespace)
   217  }
   218  
   219  func (o objectRefMeta) GVK() schema.GroupVersionKind {
   220  	return o.ObjectReference.GroupVersionKind()
   221  }
   222  
   223  // Get the "most recent pod" from the K8sRuntimeState.
   224  // For most users, we believe there will be only one pod per manifest.
   225  // So most of this time, this will return the only pod.
   226  // And in other cases, it will return a reasonable, consistent default.
   227  func (s K8sRuntimeState) MostRecentPod() v1alpha1.Pod {
   228  	return MostRecentPod(s.GetPods())
   229  }
   230  
   231  func AllPodContainers(p v1alpha1.Pod) []v1alpha1.Container {
   232  	var result []v1alpha1.Container
   233  	result = append(result, p.InitContainers...)
   234  	result = append(result, p.Containers...)
   235  	return result
   236  }
   237  
   238  func AllPodContainersReady(p v1alpha1.Pod) bool {
   239  	if len(p.Containers) == 0 {
   240  		return false
   241  	}
   242  
   243  	for _, c := range p.Containers {
   244  		if !c.Ready {
   245  			return false
   246  		}
   247  	}
   248  	return true
   249  }
   250  
   251  func AllPodContainerRestarts(p v1alpha1.Pod) int32 {
   252  	result := int32(0)
   253  	for _, c := range p.Containers {
   254  		result += c.Restarts
   255  	}
   256  	return result
   257  }
   258  
   259  func (s K8sRuntimeState) VisiblePodContainerRestarts(podID k8s.PodID) int32 {
   260  	name := string(podID)
   261  	for _, pod := range s.GetPods() {
   262  		if pod.Name == name {
   263  			return AllPodContainerRestarts(pod)
   264  		}
   265  	}
   266  	return 0
   267  }
   268  
   269  func AllPodContainerPorts(p v1alpha1.Pod) []int32 {
   270  	result := make([]int32, 0)
   271  	for _, c := range p.Containers {
   272  		result = append(result, c.Ports...)
   273  	}
   274  	return result
   275  }
   276  
   277  func MostRecentPod(list []v1alpha1.Pod) v1alpha1.Pod {
   278  	bestPod := v1alpha1.Pod{}
   279  	found := false
   280  
   281  	for _, pod := range list {
   282  		if !found || k8sconv.PodCompare(pod, bestPod) {
   283  			bestPod = pod
   284  			found = true
   285  		}
   286  	}
   287  
   288  	return bestPod
   289  }