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 }