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 }