github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/store/k8sconv/pod.go (about) 1 package k8sconv 2 3 import ( 4 "context" 5 "fmt" 6 7 "k8s.io/apimachinery/pkg/types" 8 9 "github.com/tilt-dev/tilt/pkg/apis" 10 11 "github.com/tilt-dev/tilt/pkg/model/logstore" 12 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/logger" 19 "github.com/tilt-dev/tilt/pkg/model" 20 ) 21 22 func Pod(ctx context.Context, pod *v1.Pod, ancestorUID types.UID) *v1alpha1.Pod { 23 podInfo := &v1alpha1.Pod{ 24 UID: string(pod.UID), 25 Name: pod.Name, 26 Namespace: pod.Namespace, 27 CreatedAt: apis.NewTime(pod.CreationTimestamp.Time), 28 Phase: string(pod.Status.Phase), 29 Deleting: pod.DeletionTimestamp != nil && !pod.DeletionTimestamp.IsZero(), 30 Conditions: PodConditions(pod.Status.Conditions), 31 InitContainers: PodContainers(ctx, pod, pod.Status.InitContainerStatuses), 32 Containers: PodContainers(ctx, pod, pod.Status.ContainerStatuses), 33 34 AncestorUID: string(ancestorUID), 35 PodTemplateSpecHash: pod.Labels[k8s.TiltPodTemplateHashLabel], 36 Status: PodStatusToString(*pod), 37 Errors: PodStatusErrorMessages(*pod), 38 } 39 40 if len(pod.OwnerReferences) > 0 { 41 owner := pod.OwnerReferences[0] 42 podInfo.Owner = &v1alpha1.PodOwner{ 43 Name: owner.Name, 44 APIVersion: owner.APIVersion, 45 Kind: owner.Kind, 46 } 47 } 48 49 return podInfo 50 } 51 52 func PodConditions(conditions []v1.PodCondition) []v1alpha1.PodCondition { 53 result := make([]v1alpha1.PodCondition, 0, len(conditions)) 54 for _, c := range conditions { 55 condition := v1alpha1.PodCondition{ 56 Type: string(c.Type), 57 Status: string(c.Status), 58 LastTransitionTime: apis.NewTime(c.LastTransitionTime.Time), 59 Reason: c.Reason, 60 Message: c.Message, 61 } 62 result = append(result, condition) 63 } 64 return result 65 } 66 67 // Convert a Kubernetes Pod into a list of simpler Container models to store in the engine state. 68 func PodContainers(ctx context.Context, pod *v1.Pod, containerStatuses []v1.ContainerStatus) []v1alpha1.Container { 69 result := make([]v1alpha1.Container, 0, len(containerStatuses)) 70 for _, cStatus := range containerStatuses { 71 c, err := ContainerForStatus(pod, cStatus) 72 if err != nil { 73 logger.Get(ctx).Debugf("%s", err.Error()) 74 continue 75 } 76 77 if c.Name != "" { 78 result = append(result, c) 79 } 80 } 81 return result 82 } 83 84 // Convert a Kubernetes Pod and ContainerStatus into a simpler Container model to store in the engine state. 85 func ContainerForStatus(pod *v1.Pod, cStatus v1.ContainerStatus) (v1alpha1.Container, error) { 86 cSpec := k8s.ContainerSpecOf(pod, cStatus) 87 ports := make([]int32, len(cSpec.Ports)) 88 for i, cPort := range cSpec.Ports { 89 ports[i] = cPort.ContainerPort 90 } 91 92 cID, err := k8s.NormalizeContainerID(cStatus.ContainerID) 93 if err != nil { 94 return v1alpha1.Container{}, fmt.Errorf("error parsing container ID: %w", err) 95 } 96 97 c := v1alpha1.Container{ 98 Name: cStatus.Name, 99 ID: string(cID), 100 Ready: cStatus.Ready, 101 Image: cStatus.Image, 102 Restarts: cStatus.RestartCount, 103 State: v1alpha1.ContainerState{}, 104 Ports: ports, 105 } 106 107 if cStatus.State.Waiting != nil { 108 c.State.Waiting = &v1alpha1.ContainerStateWaiting{ 109 Reason: cStatus.State.Waiting.Reason, 110 } 111 } else if cStatus.State.Running != nil { 112 c.State.Running = &v1alpha1.ContainerStateRunning{ 113 StartedAt: apis.NewTime(cStatus.State.Running.StartedAt.Time), 114 } 115 } else if cStatus.State.Terminated != nil { 116 c.State.Terminated = &v1alpha1.ContainerStateTerminated{ 117 StartedAt: apis.NewTime(cStatus.State.Terminated.StartedAt.Time), 118 FinishedAt: apis.NewTime(cStatus.State.Terminated.FinishedAt.Time), 119 Reason: cStatus.State.Terminated.Reason, 120 ExitCode: cStatus.State.Terminated.ExitCode, 121 } 122 } 123 124 return c, nil 125 } 126 127 func ContainerStatusToRuntimeState(status v1alpha1.Container) v1alpha1.RuntimeStatus { 128 state := status.State 129 if state.Terminated != nil { 130 if state.Terminated.ExitCode == 0 { 131 return v1alpha1.RuntimeStatusOK 132 } else { 133 return v1alpha1.RuntimeStatusError 134 } 135 } 136 137 if state.Waiting != nil { 138 if ErrorWaitingReasons[state.Waiting.Reason] { 139 return v1alpha1.RuntimeStatusError 140 } 141 return v1alpha1.RuntimeStatusPending 142 } 143 144 // TODO(milas): this should really consider status.Ready 145 if state.Running != nil { 146 return v1alpha1.RuntimeStatusOK 147 } 148 149 return v1alpha1.RuntimeStatusUnknown 150 } 151 152 var ErrorWaitingReasons = map[string]bool{ 153 "CrashLoopBackOff": true, 154 "ErrImagePull": true, 155 "ImagePullBackOff": true, 156 "RunContainerError": true, 157 "StartError": true, 158 "Error": true, 159 } 160 161 // SpanIDForPod creates a span ID for a given pod associated with a manifest. 162 // 163 // Generally, a given Pod is only referenced by a single manifest, but there are 164 // rare occasions where it can be referenced by multiple. If the span ID is not 165 // unique between them, things will behave erratically. 166 func SpanIDForPod(mn model.ManifestName, podID k8s.PodID) logstore.SpanID { 167 return logstore.SpanID(fmt.Sprintf("pod:%s:%s", mn.String(), podID)) 168 } 169 170 // copied from https://github.com/kubernetes/kubernetes/blob/aedeccda9562b9effe026bb02c8d3c539fc7bb77/pkg/kubectl/resource_printer.go#L692-L764 171 // to match the status column of `kubectl get pods` 172 func PodStatusToString(pod v1.Pod) string { 173 reason := string(pod.Status.Phase) 174 if pod.Status.Reason != "" { 175 reason = pod.Status.Reason 176 } 177 178 for i, container := range pod.Status.InitContainerStatuses { 179 state := container.State 180 181 switch { 182 case state.Terminated != nil && state.Terminated.ExitCode == 0: 183 continue 184 case state.Terminated != nil: 185 // initialization is failed 186 if len(state.Terminated.Reason) == 0 { 187 if state.Terminated.Signal != 0 { 188 reason = fmt.Sprintf("Init:Signal:%d", state.Terminated.Signal) 189 } else { 190 reason = fmt.Sprintf("Init:ExitCode:%d", state.Terminated.ExitCode) 191 } 192 } else { 193 reason = "Init:" + state.Terminated.Reason 194 } 195 case state.Waiting != nil && len(state.Waiting.Reason) > 0 && state.Waiting.Reason != "PodInitializing": 196 reason = "Init:" + state.Waiting.Reason 197 default: 198 reason = fmt.Sprintf("Init:%d/%d", i, len(pod.Spec.InitContainers)) 199 } 200 break 201 } 202 203 if isPodStillInitializing(pod) { 204 return reason 205 } 206 207 for i := len(pod.Status.ContainerStatuses) - 1; i >= 0; i-- { 208 container := pod.Status.ContainerStatuses[i] 209 state := container.State 210 211 if state.Waiting != nil && state.Waiting.Reason != "" { 212 reason = state.Waiting.Reason 213 } else if state.Terminated != nil && state.Terminated.Reason != "" { 214 reason = state.Terminated.Reason 215 } else if state.Terminated != nil && state.Terminated.Reason == "" { 216 if state.Terminated.Signal != 0 { 217 reason = fmt.Sprintf("Signal:%d", state.Terminated.Signal) 218 } else { 219 reason = fmt.Sprintf("ExitCode:%d", state.Terminated.ExitCode) 220 } 221 } 222 } 223 224 return reason 225 } 226 227 // Pull out interesting error messages from the pod status 228 func PodStatusErrorMessages(pod v1.Pod) []string { 229 result := []string{} 230 if isPodStillInitializing(pod) { 231 for _, container := range pod.Status.InitContainerStatuses { 232 result = append(result, containerStatusErrorMessages(container)...) 233 } 234 } 235 for i := len(pod.Status.ContainerStatuses) - 1; i >= 0; i-- { 236 container := pod.Status.ContainerStatuses[i] 237 result = append(result, containerStatusErrorMessages(container)...) 238 } 239 return result 240 } 241 242 func containerStatusErrorMessages(container v1.ContainerStatus) []string { 243 result := []string{} 244 state := container.State 245 if state.Waiting != nil { 246 lastState := container.LastTerminationState 247 if lastState.Terminated != nil && 248 lastState.Terminated.ExitCode != 0 && 249 lastState.Terminated.Message != "" { 250 result = append(result, lastState.Terminated.Message) 251 } 252 253 // If we're in an error mode, also include the error message. 254 // Many error modes put important information in the error message, 255 // like when the pod will get rescheduled. 256 if state.Waiting.Message != "" && ErrorWaitingReasons[state.Waiting.Reason] { 257 result = append(result, state.Waiting.Message) 258 } 259 } else if state.Terminated != nil && 260 state.Terminated.ExitCode != 0 && 261 state.Terminated.Message != "" { 262 result = append(result, state.Terminated.Message) 263 } 264 265 return result 266 } 267 268 func isPodStillInitializing(pod v1.Pod) bool { 269 for _, container := range pod.Status.InitContainerStatuses { 270 state := container.State 271 isFinished := state.Terminated != nil && state.Terminated.ExitCode == 0 272 if !isFinished { 273 return true 274 } 275 } 276 return false 277 }