mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-08-27 00:37:20 +00:00
515 lines
16 KiB
Go
515 lines
16 KiB
Go
package collect
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
|
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
|
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
|
|
corev1 "k8s.io/api/core/v1"
|
|
"k8s.io/client-go/kubernetes"
|
|
v1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
|
"k8s.io/client-go/rest"
|
|
"k8s.io/klog/v2"
|
|
|
|
kuberneteserrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/util/wait"
|
|
)
|
|
|
|
type CollectRunPod struct {
|
|
Collector *troubleshootv1beta2.RunPod
|
|
BundlePath string
|
|
Namespace string
|
|
ClientConfig *rest.Config
|
|
Client kubernetes.Interface
|
|
Context context.Context
|
|
RBACErrors
|
|
}
|
|
|
|
func (c *CollectRunPod) Title() string {
|
|
return getCollectorName(c)
|
|
}
|
|
|
|
func (c *CollectRunPod) IsExcluded() (bool, error) {
|
|
return isExcluded(c.Collector.Exclude)
|
|
}
|
|
|
|
func (c *CollectRunPod) Collect(progressChan chan<- interface{}) (result CollectorResult, err error) {
|
|
ctx := context.Background()
|
|
result = NewResult()
|
|
|
|
client, err := kubernetes.NewForConfig(c.ClientConfig)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to create client from config")
|
|
}
|
|
|
|
pod, err := runPodWithSpec(ctx, client, c.Collector)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to run pod")
|
|
}
|
|
defer func() {
|
|
deletePod(ctx, client, pod)
|
|
}()
|
|
|
|
if c.Collector.ImagePullSecret != nil && c.Collector.ImagePullSecret.Data != nil {
|
|
defer c.deleteImagePullSecret(context.Background(), client, pod)
|
|
}
|
|
|
|
defer func() {
|
|
if err != nil {
|
|
return
|
|
}
|
|
result, err = savePodDetails(ctx, client, result, c.BundlePath, c.ClientConfig, pod, c.Collector)
|
|
if err != nil {
|
|
klog.Errorf("failed to save pod details: %v", err)
|
|
}
|
|
}()
|
|
|
|
if c.Collector.Timeout == "" {
|
|
return runWithoutTimeout(ctx, c.BundlePath, c.ClientConfig, pod, c.Collector)
|
|
}
|
|
|
|
timeout, err := time.ParseDuration(c.Collector.Timeout)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to parse timeout")
|
|
}
|
|
|
|
errCh := make(chan error, 1)
|
|
resultCh := make(chan CollectorResult, 1)
|
|
|
|
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
|
|
defer cancel()
|
|
|
|
// TODO: Use context with timeout instead of go routine
|
|
go func() {
|
|
b, err := runWithoutTimeout(timeoutCtx, c.BundlePath, c.ClientConfig, pod, c.Collector)
|
|
if err != nil {
|
|
errCh <- err
|
|
} else {
|
|
resultCh <- b
|
|
}
|
|
}()
|
|
|
|
select {
|
|
case <-time.After(timeout):
|
|
return result, errors.New("timeout")
|
|
case output := <-resultCh:
|
|
result.AddResult(output)
|
|
return result, nil
|
|
case err := <-errCh:
|
|
return result, err
|
|
}
|
|
}
|
|
|
|
func (c *CollectRunPod) deleteImagePullSecret(ctx context.Context, client kubernetes.Interface, pod *corev1.Pod) {
|
|
for _, k := range pod.Spec.ImagePullSecrets {
|
|
secret, err := client.CoreV1().Secrets(pod.Namespace).Get(ctx, k.Name, metav1.GetOptions{})
|
|
if err != nil {
|
|
if kuberneteserrors.IsNotFound(err) {
|
|
klog.V(2).Infof("Secret %s in namespace %s not found", k.Name, pod.Namespace)
|
|
} else {
|
|
klog.Errorf("Failed to get secret %s in namespace %s: %v", k.Name, pod.Namespace, err)
|
|
}
|
|
continue
|
|
}
|
|
if secret.Labels["app.kubernetes.io/managed-by"] == "troubleshoot.sh" {
|
|
if err := client.CoreV1().Secrets(pod.Namespace).Delete(context.Background(), k.Name, metav1.DeleteOptions{}); err != nil {
|
|
klog.Errorf("Failed to delete secret %s in namespace %s: %v", k.Name, pod.Namespace, err)
|
|
} else {
|
|
klog.V(2).Infof("Deleted secret %s in namespace %s", k.Name, pod.Namespace)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func runPodWithSpec(ctx context.Context, client *kubernetes.Clientset, runPodCollector *troubleshootv1beta2.RunPod) (*corev1.Pod, error) {
|
|
pod := createPodStruct(runPodCollector)
|
|
|
|
if runPodCollector.ImagePullSecret != nil && runPodCollector.ImagePullSecret.Data != nil {
|
|
secretName, err := createSecret(ctx, client, pod.Namespace, runPodCollector.ImagePullSecret)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to create secret")
|
|
}
|
|
pod.Spec.ImagePullSecrets = append(pod.Spec.ImagePullSecrets, corev1.LocalObjectReference{Name: secretName})
|
|
}
|
|
|
|
created, err := client.CoreV1().Pods(pod.Namespace).Create(ctx, &pod, metav1.CreateOptions{})
|
|
klog.V(2).Infof("Pod %s has been created", pod.Name)
|
|
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to create pod")
|
|
}
|
|
|
|
return created, nil
|
|
}
|
|
|
|
func runWithoutTimeout(ctx context.Context, bundlePath string, clientConfig *rest.Config, pod *corev1.Pod, runPodCollector *troubleshootv1beta2.RunPod) (CollectorResult, error) {
|
|
client, err := kubernetes.NewForConfig(clientConfig)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed create client from config")
|
|
}
|
|
|
|
for {
|
|
status, err := client.CoreV1().Pods(pod.Namespace).Get(ctx, pod.Name, metav1.GetOptions{})
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to get pod")
|
|
}
|
|
if status.Status.Phase == corev1.PodRunning ||
|
|
status.Status.Phase == corev1.PodFailed ||
|
|
status.Status.Phase == corev1.PodSucceeded {
|
|
break
|
|
}
|
|
if status.Status.Phase == corev1.PodPending {
|
|
for _, v := range status.Status.ContainerStatuses {
|
|
if v.State.Waiting != nil && v.State.Waiting.Reason == "ImagePullBackOff" && !runPodCollector.AllowImagePullRetries {
|
|
return nil, errors.Errorf("run pod aborted after getting pod status 'ImagePullBackOff'")
|
|
}
|
|
|
|
if v.State.Waiting != nil && v.State.Waiting.Reason == "ContainerCreating" {
|
|
podEvents, err := client.CoreV1().Events(pod.Namespace).List(ctx, metav1.ListOptions{FieldSelector: (fmt.Sprintf("involvedObject.name=%s", pod.Name)), TypeMeta: metav1.TypeMeta{Kind: "Pod"}})
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to get pod events")
|
|
}
|
|
|
|
for _, podEvent := range podEvents.Items {
|
|
if podEvent.Reason == "FailedCreatePodSandBox" {
|
|
klog.V(2).Infof("Pod %s failed to setup network for sandbox", pod.Name)
|
|
return nil, errors.Errorf("run pod aborted after getting pod status 'FailedCreatePodSandBox'")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
time.Sleep(time.Second * 1)
|
|
}
|
|
|
|
output := NewResult()
|
|
|
|
collectorName := runPodCollector.Name
|
|
|
|
limits := troubleshootv1beta2.LogLimits{
|
|
MaxLines: 10000,
|
|
MaxBytes: 5000000,
|
|
}
|
|
podLogs, err := savePodLogs(ctx, bundlePath, client, pod, collectorName, "", &limits, true, true, false)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to get pod logs")
|
|
}
|
|
|
|
for k, v := range podLogs {
|
|
output[k] = v
|
|
}
|
|
|
|
return output, nil
|
|
}
|
|
|
|
func createSecret(ctx context.Context, client kubernetes.Interface, namespace string, imagePullSecret *troubleshootv1beta2.ImagePullSecrets) (string, error) {
|
|
if imagePullSecret.Data == nil {
|
|
return "", nil
|
|
}
|
|
|
|
var out bytes.Buffer
|
|
data := make(map[string][]byte)
|
|
if imagePullSecret.SecretType != "kubernetes.io/dockerconfigjson" {
|
|
return "", errors.Errorf("ImagePullSecret must be of type: kubernetes.io/dockerconfigjson")
|
|
}
|
|
|
|
// Check if required field in data exists
|
|
v, found := imagePullSecret.Data[".dockerconfigjson"]
|
|
if !found {
|
|
return "", errors.Errorf("Secret type kubernetes.io/dockerconfigjson requires argument \".dockerconfigjson\"")
|
|
}
|
|
if len(imagePullSecret.Data) > 1 {
|
|
return "", errors.Errorf("Secret type kubernetes.io/dockerconfigjson accepts only one argument \".dockerconfigjson\"")
|
|
}
|
|
// K8s client accepts only Json formated files as data, provided data must be decoded and indented
|
|
parsedConfig, err := base64.StdEncoding.DecodeString(v)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "Unable to decode data.")
|
|
}
|
|
err = json.Indent(&out, parsedConfig, "", "\t")
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "Unable to parse encoded data.")
|
|
}
|
|
data[".dockerconfigjson"] = out.Bytes()
|
|
|
|
secret := corev1.Secret{
|
|
TypeMeta: metav1.TypeMeta{
|
|
APIVersion: "v1",
|
|
Kind: "Secret",
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: imagePullSecret.Name,
|
|
GenerateName: "troubleshoot",
|
|
Namespace: namespace,
|
|
Labels: map[string]string{
|
|
"app.kubernetes.io/managed-by": "troubleshoot.sh",
|
|
},
|
|
},
|
|
Data: data,
|
|
Type: corev1.SecretType(imagePullSecret.SecretType),
|
|
}
|
|
|
|
created, err := client.CoreV1().Secrets(namespace).Create(ctx, &secret, metav1.CreateOptions{})
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "failed to create secret")
|
|
}
|
|
|
|
return created.Name, nil
|
|
}
|
|
|
|
// RunPodOptions and RunPodReadyNodes currently only used for the Sysctl collector
|
|
// TODO: refactor sysctl collector and runPod collector to share the same code
|
|
type RunPodOptions struct {
|
|
Image string
|
|
ImagePullPolicy string
|
|
Namespace string
|
|
Command []string
|
|
ImagePullSecretName string
|
|
HostNetwork bool
|
|
}
|
|
|
|
func RunPodsReadyNodes(ctx context.Context, client v1.CoreV1Interface, opts RunPodOptions) (map[string][]byte, error) {
|
|
wg := sync.WaitGroup{}
|
|
mtx := sync.Mutex{}
|
|
nodeLogs := map[string][]byte{}
|
|
|
|
nodes, err := client.Nodes().List(ctx, metav1.ListOptions{})
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "list nodes")
|
|
}
|
|
|
|
for _, node := range nodes.Items {
|
|
if !k8sutil.NodeIsReady(node) {
|
|
continue
|
|
}
|
|
|
|
wg.Add(1)
|
|
|
|
go func(node string) {
|
|
defer wg.Done()
|
|
|
|
pod := &corev1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
GenerateName: "run-pod-",
|
|
Namespace: opts.Namespace,
|
|
},
|
|
Spec: corev1.PodSpec{
|
|
NodeSelector: map[string]string{
|
|
"kubernetes.io/hostname": node,
|
|
},
|
|
RestartPolicy: corev1.RestartPolicyNever,
|
|
HostNetwork: opts.HostNetwork,
|
|
Containers: []corev1.Container{
|
|
{
|
|
Name: "run",
|
|
Image: opts.Image,
|
|
ImagePullPolicy: corev1.PullPolicy(opts.ImagePullPolicy),
|
|
Command: opts.Command,
|
|
},
|
|
},
|
|
Tolerations: []corev1.Toleration{
|
|
{
|
|
Key: "node-role.kubernetes.io/master",
|
|
Operator: "Exists",
|
|
Effect: "NoSchedule",
|
|
},
|
|
{
|
|
Key: "node-role.kubernetes.io/control-plane",
|
|
Operator: "Exists",
|
|
Effect: "NoSchedule",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
if opts.ImagePullSecretName != "" {
|
|
pod.Spec.ImagePullSecrets = append(pod.Spec.ImagePullSecrets, corev1.LocalObjectReference{Name: opts.ImagePullSecretName})
|
|
}
|
|
logs, err := RunPodLogs(ctx, client, pod)
|
|
if err != nil {
|
|
klog.Errorf("Failed to run pod on node %s: %v", node, err)
|
|
return
|
|
}
|
|
|
|
mtx.Lock()
|
|
defer mtx.Unlock()
|
|
nodeLogs[node] = logs
|
|
}(node.Name)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
return nodeLogs, nil
|
|
}
|
|
|
|
// RunPodLogs runs a pod to completion on a node and returns its logs
|
|
func RunPodLogs(ctx context.Context, client v1.CoreV1Interface, podSpec *corev1.Pod) ([]byte, error) {
|
|
return RunPodLogsWithOptions(ctx, client, podSpec, false)
|
|
}
|
|
|
|
// RunPodLogsWithOptions runs a pod to completion on a node and returns its logs with configurable options
|
|
func RunPodLogsWithOptions(ctx context.Context, client v1.CoreV1Interface, podSpec *corev1.Pod, allowImagePullRetries bool) ([]byte, error) {
|
|
// 1. Create
|
|
pod, err := client.Pods(podSpec.Namespace).Create(ctx, podSpec, metav1.CreateOptions{})
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to create pod")
|
|
}
|
|
defer func() {
|
|
err := client.Pods(pod.Namespace).Delete(context.Background(), pod.Name, metav1.DeleteOptions{})
|
|
if err != nil && !kuberneteserrors.IsNotFound(err) {
|
|
klog.Errorf("Failed to delete pod %s: %v\n", pod.Name, err)
|
|
}
|
|
}()
|
|
|
|
// 2. Wait
|
|
for {
|
|
pod, err := client.Pods(pod.Namespace).Get(ctx, pod.Name, metav1.GetOptions{})
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to get pod")
|
|
}
|
|
|
|
if pod.Status.Phase == corev1.PodFailed || pod.Status.Phase == corev1.PodSucceeded {
|
|
break
|
|
}
|
|
|
|
if pod.Status.Phase == corev1.PodPending {
|
|
for _, v := range pod.Status.ContainerStatuses {
|
|
if v.State.Waiting != nil && v.State.Waiting.Reason == "ImagePullBackOff" && !allowImagePullRetries {
|
|
return nil, errors.New("wait for pod aborted after getting pod status 'ImagePullBackOff'")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. Logs
|
|
podLogOpts := corev1.PodLogOptions{
|
|
Container: pod.Spec.Containers[0].Name,
|
|
Follow: true,
|
|
}
|
|
req := client.Pods(pod.Namespace).GetLogs(pod.Name, &podLogOpts)
|
|
logs, err := req.Stream(ctx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to get log stream")
|
|
}
|
|
defer logs.Close()
|
|
|
|
return io.ReadAll(logs)
|
|
}
|
|
|
|
func savePodDetails(ctx context.Context, client *kubernetes.Clientset, output CollectorResult, bundlePath string, clientConfig *rest.Config, pod *corev1.Pod, runPodCollector *troubleshootv1beta2.RunPod) (CollectorResult, error) {
|
|
podStatus, err := client.CoreV1().Pods(pod.Namespace).Get(ctx, pod.Name, metav1.GetOptions{})
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to get pod")
|
|
}
|
|
|
|
podEvents, err := client.CoreV1().Events(pod.Namespace).List(ctx, metav1.ListOptions{FieldSelector: (fmt.Sprintf("involvedObject.name=%s", pod.Name)), TypeMeta: metav1.TypeMeta{Kind: "Pod"}})
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to get pod events")
|
|
}
|
|
|
|
podBytes, err := json.MarshalIndent(podStatus, "", " ")
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to marshal pod status")
|
|
}
|
|
|
|
podEventBytes, err := json.MarshalIndent(podEvents, "", " ")
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to marshal pod events")
|
|
}
|
|
|
|
err = output.SaveResult(bundlePath, filepath.Join(runPodCollector.Name, fmt.Sprintf("%s.json", runPodCollector.Name)), bytes.NewBuffer(podBytes))
|
|
if err != nil {
|
|
klog.Errorf("failed to save pod status results to %s.json: %v", runPodCollector.Name, err)
|
|
}
|
|
|
|
err = output.SaveResult(bundlePath, filepath.Join(runPodCollector.Name, fmt.Sprintf("%s-events.json", runPodCollector.Name)), bytes.NewBuffer(podEventBytes))
|
|
if err != nil {
|
|
klog.Errorf("failed to save pod event results to %s-events.json: %v", runPodCollector.Name, err)
|
|
}
|
|
return output, nil
|
|
}
|
|
|
|
func deletePod(ctx context.Context, client *kubernetes.Clientset, pod *corev1.Pod) {
|
|
if err := client.CoreV1().Pods(pod.Namespace).Delete(context.Background(), pod.Name, metav1.DeleteOptions{}); err != nil {
|
|
klog.Errorf("Failed to delete pod %s: %v", pod.Name, err)
|
|
return
|
|
}
|
|
klog.V(2).Infof("Pod %s has been scheduled for deletion", pod.Name)
|
|
|
|
// Wait until the pod is deleted
|
|
// Poll every second to check if the Pod has been deleted.
|
|
klog.V(2).Infof("Continuously poll each second for Pod %s deletion for maximum %d seconds", pod.Name, constants.MAX_TIME_TO_WAIT_FOR_POD_DELETION/time.Second)
|
|
err := wait.PollUntilContextTimeout(ctx, time.Second, constants.MAX_TIME_TO_WAIT_FOR_POD_DELETION, true, func(ctx context.Context) (bool, error) {
|
|
_, getErr := client.CoreV1().Pods(pod.Namespace).Get(ctx, pod.Name, metav1.GetOptions{})
|
|
// If the Pod is not found, it has been deleted.
|
|
if kuberneteserrors.IsNotFound(getErr) {
|
|
return true, nil
|
|
}
|
|
// If there is an error from context (e.g., context deadline exceeded), return the error.
|
|
if ctx.Err() != nil {
|
|
return false, ctx.Err()
|
|
}
|
|
// Otherwise, the Pod has not yet been deleted. Keep polling.
|
|
return false, nil
|
|
})
|
|
if err != nil {
|
|
zeroGracePeriod := int64(0)
|
|
klog.V(2).Infof("Forcefully deleting pod %s after reaching the maximum wait time of %d seconds due to err=%v",
|
|
pod.Name, constants.MAX_TIME_TO_WAIT_FOR_POD_DELETION/time.Second, err,
|
|
)
|
|
if err := client.CoreV1().Pods(pod.Namespace).Delete(context.Background(), pod.Name, metav1.DeleteOptions{
|
|
GracePeriodSeconds: &zeroGracePeriod,
|
|
}); err != nil {
|
|
klog.Errorf("Forced deletion of pod %s failed: %v", pod.Name, err)
|
|
return
|
|
}
|
|
klog.V(2).Infof("Pod %s in %s namespace has been deleted", pod.Name, pod.Namespace)
|
|
} else {
|
|
klog.V(2).Infof("Pod %s in %s namespace has been deleted", pod.Name, pod.Namespace)
|
|
}
|
|
}
|
|
|
|
func createPodStruct(runPodCollector *troubleshootv1beta2.RunPod) corev1.Pod {
|
|
podLabels := make(map[string]string)
|
|
podLabels["troubleshoot-role"] = "run-collector"
|
|
|
|
namespace := "default"
|
|
if runPodCollector.Namespace != "" {
|
|
namespace = runPodCollector.Namespace
|
|
}
|
|
|
|
podName := "run-pod"
|
|
if runPodCollector.CollectorName != "" {
|
|
podName = runPodCollector.CollectorName
|
|
} else if runPodCollector.Name != "" {
|
|
podName = runPodCollector.Name
|
|
}
|
|
|
|
pod := corev1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: podName,
|
|
Namespace: namespace,
|
|
Labels: podLabels,
|
|
Annotations: runPodCollector.Annotations,
|
|
},
|
|
TypeMeta: metav1.TypeMeta{
|
|
APIVersion: "v1",
|
|
Kind: "Pod",
|
|
},
|
|
Spec: runPodCollector.PodSpec,
|
|
}
|
|
|
|
return pod
|
|
}
|