-
Notifications
You must be signed in to change notification settings - Fork 102
AAP-46484 Capture k8s pod diagnostics #1332
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 52 commits
55b3047
b27cda5
a174a48
cb9a50b
88414e8
64b7e79
41a3d29
49afc98
a6b1263
2fb30e2
362a739
a5527cb
f520d48
7c8926c
735f110
5dead7d
d56c4c6
3ecf827
9b938c7
22939d4
c528965
c021f09
bcbc220
03d347a
1f42903
0f11125
3a20984
e0df53d
c4db94b
30743b8
9608c06
bc6320c
3f6de50
0e37103
f0c3698
0db61e5
fd67a69
3a33387
cc16392
95e21e0
7424c77
71c5e47
7002182
677c2b3
65604ab
c73562e
568681c
78358bf
0c1f67d
1ad624c
2fad163
6e02cf3
4ac5aca
6cb833e
b6c66a9
3f10768
5aaf190
9c35093
3db8495
fd4a5c0
afca14c
0a7e38b
8bc7f36
e477eb8
e5da583
bf86624
2bb31a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,6 +41,7 @@ | |
| // KubeUnit implements the WorkUnit interface. | ||
| type KubeUnit struct { | ||
| BaseWorkUnitForWorkUnit | ||
| KubePodStateHelper | ||
| KubeAPIWrapperInstance KubeAPIer | ||
| authMethod string | ||
| streamMethod string | ||
|
|
@@ -90,7 +91,8 @@ | |
| NewFakeAlwaysRateLimiter() flowcontrol.RateLimiter | ||
| } | ||
|
|
||
| type KubeAPIWrapper struct{} | ||
| type KubeAPIWrapper struct { | ||
| } | ||
|
|
||
| func (ku KubeAPIWrapper) NewNotFound(qualifiedResource schema.GroupResource, name string) *apierrors.StatusError { | ||
| return apierrors.NewNotFound(qualifiedResource, name) | ||
|
|
@@ -177,6 +179,8 @@ | |
| // ErrImagePullBackOff is returned when the image for the container in the Pod cannot be pulled. | ||
| var ErrImagePullBackOff = fmt.Errorf("container failed to start") | ||
|
|
||
| const containerName = "worker" | ||
|
|
||
| // podRunningAndReady is a completion criterion for pod ready to be attached to. | ||
| func podRunningAndReady(kw KubeUnit) func(event watch.Event) (bool, error) { | ||
| imagePullBackOffRetries := 3 | ||
|
|
@@ -249,7 +253,7 @@ | |
| podNamespace := kw.Pod.Namespace | ||
| podName := kw.Pod.Name | ||
| podOptions := &corev1.PodLogOptions{ | ||
| Container: "worker", | ||
| Container: containerName, | ||
| Follow: true, | ||
| } | ||
| if timestamps { | ||
|
|
@@ -397,6 +401,15 @@ | |
| podName, | ||
| ) | ||
|
|
||
| // timeout := int64(10) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Take out commented out code please, The tests are enough for now
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| // _, err = kw.CapturePodStatus(kw.Pod, stdout.Size(), &timeout) | ||
| // if err != nil { | ||
| // kw.GetWorkceptor().nc.GetLogger().Info("Detected error while retrieving pod status for %s/%s.", | ||
| // podNamespace, | ||
| // podName, | ||
| // ) | ||
| // } | ||
|
|
||
|
lranjbar marked this conversation as resolved.
|
||
| return | ||
| } | ||
|
|
||
|
|
@@ -487,7 +500,7 @@ | |
| foundWorker := false | ||
| spec = &pod.Spec | ||
| for i := range spec.Containers { | ||
| if spec.Containers[i].Name == "worker" { | ||
| if spec.Containers[i].Name == containerName { | ||
| spec.Containers[i].Stdin = true | ||
| spec.Containers[i].StdinOnce = true | ||
| foundWorker = true | ||
|
|
@@ -518,7 +531,7 @@ | |
| } | ||
| spec = &corev1.PodSpec{ | ||
| Containers: []corev1.Container{{ | ||
| Name: "worker", | ||
| Name: containerName, | ||
| Image: ked.Image, | ||
| Command: command, | ||
| Args: params, | ||
|
|
@@ -602,7 +615,7 @@ | |
| if err == ErrPodCompleted { | ||
| // Hao: shouldn't we also call kw.Cancel() in these cases? | ||
| for _, cstat := range kw.Pod.Status.ContainerStatuses { | ||
| if cstat.Name == "worker" { | ||
| if cstat.Name == containerName { | ||
| if cstat.State.Terminated != nil && cstat.State.Terminated.ExitCode != 0 { | ||
| return fmt.Errorf("container failed with exit code %d: %s", cstat.State.Terminated.ExitCode, cstat.State.Terminated.Message) | ||
| } | ||
|
|
@@ -633,7 +646,7 @@ | |
| } | ||
|
|
||
| for _, cstat := range kw.Pod.Status.ContainerStatuses { | ||
| if cstat.Name == "worker" { | ||
| if cstat.Name == containerName { | ||
| if cstat.State.Waiting != nil { | ||
| return fmt.Errorf("%s, %s", err.Error(), cstat.State.Waiting.Reason) | ||
| } | ||
|
|
@@ -733,7 +746,7 @@ | |
|
|
||
| req.VersionedParams( | ||
| &corev1.PodExecOptions{ | ||
| Container: "worker", | ||
| Container: containerName, | ||
| Stdin: true, | ||
| Stdout: false, | ||
| Stderr: false, | ||
|
|
@@ -913,7 +926,21 @@ | |
| return | ||
| } | ||
|
|
||
| // only transition from WorkStateRunning to WorkStateSucceeded if WorkStateFailed is set we do not override | ||
| // pod, err := kw.KubeAPIWrapperInstance.Get(kw.GetContext(), kw.clientset, podNamespace, podName, metav1.GetOptions{}) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same as above The tests are enough for now |
||
| // if err != nil { | ||
| // kw.GetWorkceptor().nc.GetLogger().Warning("Failed to retrieve pod for diagnostics: %v", err) | ||
| // } | ||
| // timeout := int64(10) | ||
| // ok, _ := kw.CapturePodStatus(pod, stdout.Size(), &timeout) | ||
| // if !ok { | ||
| // // If the pod did not succeed, we already updated the status to WorkStateFailed | ||
| // // and we can return early. | ||
| // return | ||
| // } | ||
|
lranjbar marked this conversation as resolved.
|
||
|
|
||
| // Only transition to WorkStateSucceeded if the work unit is still running | ||
| // and has not already failed due to diagnostic or streaming errors. | ||
| // This ensures we don't override a previously set WorkStateFailed. | ||
| if kw.GetContext().Err() != context.Canceled && kw.Status().State == WorkStateRunning { | ||
| kw.UpdateBasicStatus(WorkStateSucceeded, "Finished", stdout.Size()) | ||
| } | ||
|
|
@@ -1556,7 +1583,7 @@ | |
| } | ||
|
|
||
| // Cancel releases resources associated with a job, including cancelling it if running. | ||
| func (kw *KubeUnit) Cancel() error { | ||
| kw.CancelContext() | ||
| kw.UpdateBasicStatus(WorkStateCanceled, "Canceled", -1) | ||
| if kw.Pod != nil { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| package workceptor | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
|
|
||
| corev1 "k8s.io/api/core/v1" | ||
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| "k8s.io/apimachinery/pkg/watch" | ||
| "k8s.io/client-go/kubernetes" | ||
| ) | ||
|
|
||
| type KubePodStateHelper interface { | ||
| CapturePodStatus(pod *corev1.Pod, stdoutSize int64, timeoutSeconds *int64) (ok bool, err error) | ||
| GetPodStatus(pod *corev1.Pod) (bool, string, error) | ||
| PodContainerHealthy(pod *corev1.Pod, containerName string) (bool, error) | ||
| PodHealthy(pod *corev1.Pod, containerName string) (bool, string, error) | ||
| WaitForPodCompleted(pod *corev1.Pod, clientset kubernetes.Interface, timeoutSeconds *int64) (*corev1.Pod, error) | ||
| } | ||
|
|
||
| func (kw *KubeUnit) CapturePodStatus(pod *corev1.Pod, stdoutSize int64, timeoutSeconds *int64) (bool, error) { | ||
| if pod == nil { | ||
| return false, fmt.Errorf("pod is nil") | ||
| } | ||
|
|
||
| if pod.Status.Phase == corev1.PodRunning || pod.Status.Phase == corev1.PodPending { | ||
| err := kw.WaitForPodCompleted(kw.GetContext(), pod, kw.clientset, timeoutSeconds) | ||
| if err != nil { | ||
| kw.GetWorkceptor().nc.GetLogger().Debug("Pod error detected while waiting for completion: %v", err) | ||
|
lranjbar marked this conversation as resolved.
Outdated
|
||
| } | ||
| } | ||
|
|
||
| ok, err := kw.GetPodStatus(pod) | ||
| if !ok || err != nil { | ||
| kw.GetWorkceptor().nc.GetLogger().Warning("Pod did not succeed: %v", err) | ||
| kw.UpdateBasicStatus(WorkStateFailed, err.Error(), stdoutSize) | ||
|
|
||
| return false, err | ||
| } | ||
|
|
||
| kw.GetWorkceptor().nc.GetLogger().Debug("Pod status: %s", pod.Status.String()) | ||
|
|
||
| return true, nil | ||
| } | ||
|
|
||
| // GetPodStatus checks if the pod has successfully completed its application logic and infrastructure is healthy. | ||
| func (ku KubeUnit) GetPodStatus(pod *corev1.Pod) (bool, error) { | ||
| if pod == nil { | ||
| return false, fmt.Errorf("pod is nil") | ||
| } | ||
|
|
||
| podRef := fmt.Sprintf("pod %s/%s", pod.Namespace, pod.Name) | ||
|
lranjbar marked this conversation as resolved.
Outdated
|
||
|
|
||
| ok, err := ku.PodHealthy(pod, containerName) | ||
| if !ok || err != nil { | ||
| return ok, fmt.Errorf("%s %s", podRef, err) | ||
| } | ||
|
|
||
| return ok, nil | ||
| } | ||
|
|
||
| // PodContainerHealthy checks if the pod has successfully completed its application logic. | ||
| // this is called after podInfrastructureSuccess has confirmed the pod is in a terminal state. | ||
| func (ku KubeUnit) PodContainerHealthy(pod *corev1.Pod, containerName string) (bool, error) { | ||
| if pod == nil { | ||
| return false, fmt.Errorf("pod is nil") | ||
| } | ||
|
|
||
| for _, cs := range pod.Status.ContainerStatuses { | ||
| if cs.Name == containerName { | ||
| if cs.State.Terminated == nil { // means it is waiting or running, so application logic has not completed yet. Normal behavior when job completes successfully. | ||
| return true, nil | ||
| } | ||
|
|
||
| if cs.State.Terminated.ExitCode != 0 { // exit code of 0 means success | ||
| return false, fmt.Errorf("container %s exited with code %d: %s", cs.Name, cs.State.Terminated.ExitCode, cs.State.Terminated.Reason) | ||
| } | ||
|
|
||
| return true, nil // container terminated with exit code of 0 | ||
| } | ||
| } | ||
|
|
||
| return false, fmt.Errorf("pod %s/%s does not contain container %s", pod.Namespace, pod.Name, containerName) | ||
|
lranjbar marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| // PodInfrastructureSuccess checks if the pod has either successfully started, is pending or is running, or has is successfully terminated. | ||
| // Any other state is considered an infrastructure failure. | ||
| func (ku KubeUnit) PodHealthy(pod *corev1.Pod, containerName string) (bool, error) { | ||
| if pod == nil { | ||
| return false, fmt.Errorf("pod is nil") | ||
| } | ||
|
|
||
| for _, cs := range pod.Status.ContainerStatuses { | ||
| // Check if this is the container we care about first | ||
| if cs.Name == containerName { | ||
| switch pod.Status.Phase { | ||
| case corev1.PodFailed: | ||
| ok, err := ku.PodContainerHealthy(pod, containerName) | ||
| if !ok || err != nil { | ||
|
|
||
| return false, err | ||
| } | ||
|
|
||
| return true, nil | ||
| case corev1.PodSucceeded: | ||
| return true, nil | ||
| case corev1.PodRunning, corev1.PodPending: | ||
| return true, nil | ||
| default: | ||
| return false, fmt.Errorf("unknown phase: %s", pod.Status.Phase) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return false, fmt.Errorf("pod %s/%s does not contain container %s", pod.Namespace, pod.Name, containerName) | ||
|
lranjbar marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| func (ku KubeUnit) WaitForPodCompleted(ctx context.Context, pod *corev1.Pod, clientset kubernetes.Interface, timeoutSeconds *int64) error { | ||
| if pod == nil { | ||
| return fmt.Errorf("pod is nil") | ||
| } | ||
|
|
||
| watcher, err := clientset.CoreV1().Pods(pod.Namespace).Watch(ctx, metav1.ListOptions{ | ||
| TimeoutSeconds: timeoutSeconds, | ||
| FieldSelector: "involvedObject.kind=Pod,involvedObject.name=" + pod.Name}) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| for event := range watcher.ResultChan() { | ||
| if event.Type == watch.Error { | ||
| return event.Object.(error) | ||
| } | ||
| pod = event.Object.(*corev1.Pod) | ||
| fmt.Printf("%s: %s/%s (Phase: %s)\n", event.Type, pod.Namespace, pod.Name, pod.Status.Phase) | ||
|
lranjbar marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| return nil | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.