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  }