github.com/grahambrereton-form3/tilt@v0.10.18/internal/engine/pod.go (about)

     1  package engine
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"time"
     7  
     8  	"github.com/pkg/errors"
     9  	v1 "k8s.io/api/core/v1"
    10  
    11  	"github.com/windmilleng/tilt/internal/container"
    12  	"github.com/windmilleng/tilt/internal/engine/k8swatch"
    13  	"github.com/windmilleng/tilt/internal/engine/runtimelog"
    14  	"github.com/windmilleng/tilt/internal/k8s"
    15  	"github.com/windmilleng/tilt/internal/store"
    16  	"github.com/windmilleng/tilt/internal/synclet/sidecar"
    17  	"github.com/windmilleng/tilt/pkg/logger"
    18  	"github.com/windmilleng/tilt/pkg/model"
    19  )
    20  
    21  func handlePodChangeAction(ctx context.Context, state *store.EngineState, action k8swatch.PodChangeAction) {
    22  	mt := matchPodChangeToManifest(state, action)
    23  	if mt == nil {
    24  		return
    25  	}
    26  
    27  	pod := action.Pod
    28  	ms := mt.State
    29  	manifest := mt.Manifest
    30  	podInfo, isNew := trackPod(ms, action)
    31  	podID := k8s.PodIDFromPod(pod)
    32  	if podInfo.PodID != podID {
    33  		// This is an event from an old pod.
    34  		return
    35  	}
    36  
    37  	// Update the status
    38  	podInfo.Deleting = pod.DeletionTimestamp != nil && !pod.DeletionTimestamp.IsZero()
    39  	podInfo.Phase = pod.Status.Phase
    40  	podInfo.Status = k8swatch.PodStatusToString(*pod)
    41  	podInfo.StatusMessages = k8swatch.PodStatusErrorMessages(*pod)
    42  
    43  	prunePods(ms)
    44  
    45  	oldRestartTotal := podInfo.AllContainerRestarts()
    46  	podInfo.Containers = podContainers(ctx, pod)
    47  	if isNew {
    48  		// This is the first time we've seen this pod.
    49  		// Ignore any restarts that happened before Tilt saw it.
    50  		//
    51  		// This can happen when the image was deployed on a previous
    52  		// Tilt run, so we're just attaching to an existing pod
    53  		// with some old history.
    54  		podInfo.BaselineRestarts = podInfo.AllContainerRestarts()
    55  	}
    56  
    57  	if len(podInfo.Containers) == 0 {
    58  		// not enough info to do anything else
    59  		return
    60  	}
    61  
    62  	if podInfo.AllContainersReady() {
    63  		runtime := ms.K8sRuntimeState()
    64  		runtime.LastReadyTime = time.Now()
    65  		ms.RuntimeState = runtime
    66  	}
    67  
    68  	fwdsValid := portForwardsAreValid(manifest, *podInfo)
    69  	if !fwdsValid {
    70  		logger.Get(ctx).Infof(
    71  			"WARNING: Resource %s is using port forwards, but no container ports on pod %s",
    72  			manifest.Name, podInfo.PodID)
    73  	}
    74  	checkForContainerCrash(ctx, state, mt)
    75  
    76  	if oldRestartTotal < podInfo.AllContainerRestarts() {
    77  		ms.CrashLog = podInfo.CurrentLog
    78  		podInfo.CurrentLog = model.Log{}
    79  	}
    80  }
    81  
    82  // Find the ManifestTarget for the PodChangeAction,
    83  // and confirm that it matches what we've deployed.
    84  func matchPodChangeToManifest(state *store.EngineState, action k8swatch.PodChangeAction) *store.ManifestTarget {
    85  	manifestName := action.ManifestName
    86  	ancestorUID := action.AncestorUID
    87  	mt, ok := state.ManifestTargets[manifestName]
    88  	if !ok {
    89  		// This is OK. The user could have edited the manifest recently.
    90  		return nil
    91  	}
    92  
    93  	ms := mt.State
    94  	runtime := ms.GetOrCreateK8sRuntimeState()
    95  
    96  	// If the event has an ancestor UID attached, but that ancestor isn't in the
    97  	// deployed UID set anymore, we can ignore it.
    98  	isAncestorMatch := ancestorUID != ""
    99  	if isAncestorMatch && !runtime.DeployedUIDSet.Contains(ancestorUID) {
   100  		return nil
   101  	}
   102  	return mt
   103  }
   104  
   105  // Checks the runtime state if we're already tracking this pod.
   106  // If not, create a new tracking object.
   107  // Returns a store.Pod that the caller can mutate, and true
   108  // if this is the first time we've seen this pod.
   109  func trackPod(ms *store.ManifestState, action k8swatch.PodChangeAction) (*store.Pod, bool) {
   110  	pod := action.Pod
   111  	podID := k8s.PodIDFromPod(pod)
   112  	startedAt := pod.CreationTimestamp.Time
   113  	status := k8swatch.PodStatusToString(*pod)
   114  	ns := k8s.NamespaceFromPod(pod)
   115  	hasSynclet := sidecar.PodSpecContainsSynclet(pod.Spec)
   116  	runtime := ms.GetOrCreateK8sRuntimeState()
   117  
   118  	// Case 1: We haven't seen pods for this ancestor yet.
   119  	ancestorUID := action.AncestorUID
   120  	isAncestorMatch := ancestorUID != ""
   121  	if runtime.PodAncestorUID == "" ||
   122  		(isAncestorMatch && runtime.PodAncestorUID != ancestorUID) {
   123  		runtime.PodAncestorUID = ancestorUID
   124  		runtime.Pods = make(map[k8s.PodID]*store.Pod)
   125  		pod := &store.Pod{
   126  			PodID:      podID,
   127  			StartedAt:  startedAt,
   128  			Status:     status,
   129  			Namespace:  ns,
   130  			HasSynclet: hasSynclet,
   131  		}
   132  		runtime.Pods[podID] = pod
   133  		ms.RuntimeState = runtime
   134  		return pod, true
   135  	}
   136  
   137  	podInfo, ok := runtime.Pods[podID]
   138  	if !ok {
   139  		// CASE 2: We have a set of pods for this ancestor UID, but not this
   140  		// particular pod -- record it
   141  		podInfo = &store.Pod{
   142  			PodID:      podID,
   143  			StartedAt:  startedAt,
   144  			Status:     status,
   145  			Namespace:  ns,
   146  			HasSynclet: hasSynclet,
   147  		}
   148  		runtime.Pods[podID] = podInfo
   149  		return podInfo, true
   150  	}
   151  
   152  	// CASE 3: This pod is already in the PodSet, nothing to do.
   153  	return podInfo, false
   154  }
   155  
   156  // Convert a Kubernetes Pod into a list if simpler Container models to store in the engine state.
   157  func podContainers(ctx context.Context, pod *v1.Pod) []store.Container {
   158  	result := make([]store.Container, 0, len(pod.Status.ContainerStatuses))
   159  	for _, cStatus := range pod.Status.ContainerStatuses {
   160  		c, err := containerForStatus(ctx, pod, cStatus)
   161  		if err != nil {
   162  			logger.Get(ctx).Debugf(err.Error())
   163  			continue
   164  		}
   165  
   166  		if !c.Empty() {
   167  			result = append(result, c)
   168  		}
   169  	}
   170  	return result
   171  }
   172  
   173  // Convert a Kubernetes Pod and ContainerStatus into a simpler Container model to store in the engine state.
   174  func containerForStatus(ctx context.Context, pod *v1.Pod, cStatus v1.ContainerStatus) (store.Container, error) {
   175  	if cStatus.Name == sidecar.SyncletContainerName {
   176  		// We don't want logs, status, etc. for the Tilt synclet.
   177  		return store.Container{}, nil
   178  	}
   179  
   180  	cName := k8s.ContainerNameFromContainerStatus(cStatus)
   181  
   182  	cID, err := k8s.ContainerIDFromContainerStatus(cStatus)
   183  	if err != nil {
   184  		return store.Container{}, errors.Wrap(err, "Error parsing container ID")
   185  	}
   186  
   187  	cRef, err := container.ParseNamed(cStatus.Image)
   188  	if err != nil {
   189  		return store.Container{}, errors.Wrap(err, "Error parsing container image ID")
   190  
   191  	}
   192  
   193  	ports := make([]int32, 0)
   194  	cSpec := k8s.ContainerSpecOf(pod, cStatus)
   195  	for _, cPort := range cSpec.Ports {
   196  		ports = append(ports, cPort.ContainerPort)
   197  	}
   198  
   199  	return store.Container{
   200  		Name:     cName,
   201  		ID:       cID,
   202  		Ports:    ports,
   203  		Ready:    cStatus.Ready,
   204  		ImageRef: cRef,
   205  		Restarts: int(cStatus.RestartCount),
   206  	}, nil
   207  }
   208  
   209  func checkForContainerCrash(ctx context.Context, state *store.EngineState, mt *store.ManifestTarget) {
   210  	ms := mt.State
   211  	if ms.NeedsRebuildFromCrash {
   212  		// We're already aware the pod is crashing.
   213  		return
   214  	}
   215  
   216  	runningContainers := store.AllRunningContainers(mt)
   217  	hitList := make(map[container.ID]bool, len(ms.LiveUpdatedContainerIDs))
   218  	for cID := range ms.LiveUpdatedContainerIDs {
   219  		hitList[cID] = true
   220  	}
   221  	for _, c := range runningContainers {
   222  		delete(hitList, c.ContainerID)
   223  	}
   224  
   225  	if len(hitList) == 0 {
   226  		// The pod is what we expect it to be.
   227  		return
   228  	}
   229  
   230  	// The pod isn't what we expect!
   231  	// TODO(nick): We should store the logs by container ID, and
   232  	// only put the container that crashed in the CrashLog.
   233  	ms.CrashLog = ms.MostRecentPod().CurrentLog
   234  	ms.NeedsRebuildFromCrash = true
   235  	ms.LiveUpdatedContainerIDs = container.NewIDSet()
   236  	msg := fmt.Sprintf("Detected a container change for %s. We could be running stale code. Rebuilding and deploying a new image.", ms.Name)
   237  	le := store.NewLogEvent(ms.Name, []byte(msg+"\n"))
   238  	if len(ms.BuildHistory) > 0 {
   239  		ms.BuildHistory[0].Log = model.AppendLog(ms.BuildHistory[0].Log, le, state.LogTimestamps, "", state.Secrets)
   240  	}
   241  	ms.CurrentBuild.Log = model.AppendLog(ms.CurrentBuild.Log, le, state.LogTimestamps, "", state.Secrets)
   242  	handleLogAction(state, le)
   243  }
   244  
   245  // If there's more than one pod, prune the deleting/dead ones so
   246  // that they don't clutter the output.
   247  func prunePods(ms *store.ManifestState) {
   248  	// Always remove pods that were manually deleted.
   249  	runtime := ms.GetOrCreateK8sRuntimeState()
   250  	for key, pod := range runtime.Pods {
   251  		if pod.Deleting {
   252  			delete(runtime.Pods, key)
   253  		}
   254  	}
   255  	// Continue pruning until we have 1 pod.
   256  	for runtime.PodLen() > 1 {
   257  		bestPod := ms.MostRecentPod()
   258  
   259  		for key, pod := range runtime.Pods {
   260  			// Remove terminated pods if they aren't the most recent one.
   261  			isDead := pod.Phase == v1.PodSucceeded || pod.Phase == v1.PodFailed
   262  			if isDead && pod.PodID != bestPod.PodID {
   263  				delete(runtime.Pods, key)
   264  				break
   265  			}
   266  		}
   267  
   268  		// found nothing to delete, break out
   269  		return
   270  	}
   271  }
   272  
   273  func handlePodLogAction(state *store.EngineState, action runtimelog.PodLogAction) {
   274  	manifestName := action.Source()
   275  	ms, ok := state.ManifestState(manifestName)
   276  	if !ok {
   277  		// This is OK. The user could have edited the manifest recently.
   278  		return
   279  	}
   280  
   281  	podID := action.PodID
   282  	runtime := ms.GetOrCreateK8sRuntimeState()
   283  	if !runtime.ContainsID(podID) {
   284  		// NOTE(nick): There are two cases where this could happen:
   285  		// 1) Pod 1 died and kubernetes started Pod 2. What should we do with
   286  		//    logs from Pod 1 that are still in the action queue?
   287  		//    This is an open product question. A future HUD may aggregate
   288  		//    logs across pod restarts.
   289  		// 2) Due to race conditions, we got the logs for Pod 1 before
   290  		//    we saw Pod 1 materialize on the Pod API. The best way to fix
   291  		//    this would be to make PodLogManager a subscriber that only
   292  		//    starts listening on logs once the pod has materialized.
   293  		//    We may prioritize this higher or lower based on how often
   294  		//    this happens in practice.
   295  		return
   296  	}
   297  
   298  	podInfo := runtime.Pods[podID]
   299  	podInfo.CurrentLog = model.AppendLog(podInfo.CurrentLog, action, state.LogTimestamps, "", state.Secrets)
   300  }
   301  
   302  func handlePodResetRestartsAction(state *store.EngineState, action store.PodResetRestartsAction) {
   303  	ms, ok := state.ManifestState(action.ManifestName)
   304  	if !ok {
   305  		return
   306  	}
   307  
   308  	runtime := ms.K8sRuntimeState()
   309  	podInfo, ok := runtime.Pods[action.PodID]
   310  	if !ok {
   311  		return
   312  	}
   313  
   314  	// We have to be careful here because the pod might have restarted
   315  	// since the action was created.
   316  	delta := podInfo.VisibleContainerRestarts() - action.VisibleRestarts
   317  	podInfo.BaselineRestarts = podInfo.AllContainerRestarts() - delta
   318  }