Operator实战4:如何获取已经被删除的pod的日记

原创 吴就业 205 0 2023-07-15

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/fa8546a6ecee40d6bb0575973546d25b

作者:吴就业
链接:https://wujiuye.com/article/fa8546a6ecee40d6bb0575973546d25b
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

云原生企业级实战笔记专栏

需求背景

kubectl logs -p命令并不能获取已经删除的pod的容器日记,只适用于pod只是重启还未被删除的情况,能够获取重启前的容器的日记。

在Job场景,如果Job达到backoffLimit还是失败,而且backoffLimit值很小,很快就重试完,没能及时的获取到容器的日记。而达到backoffLimit后,job的controller会把pod删掉,这种情况就没办法获取pod的日记了,导致无法得知job执行失败的真正原因,只能看到job给的错误:”Job has reached the specified backoff limit”。

解决方案

一种方案就是添加一个controller(operator),监听terraform job创建的pod,然后给pod添加finalizers,然后监听job的删除,在真正删除之前先获取pod的容器的日记,将日记通过configmap保存下来,或者发送event。

有一个缺陷是修改不了Job的状态,因为在job的controller删除pod之后,会更新覆盖job的状态。

案例

这里以我们实际遇到的问题为案例,就是修改terraform apply、terraform destroy的job的backoffLimit为0后,我们无法获取真正terraform执行失败的原因。因此这个案例就是为了能够获取terraform-executor容器的日记。

由于terraform-controller会不断刷新configuration资源的状态,我们无法修改configuration资源的状态,因为一修改就会被terraform-controller覆盖。所以无法直接在kubevela上看到组件真正的失败原因。

需要注意的是,controller不要监听所有的pod。此案例中,我们应该只关心terraform的job创建的pod,因此controller需要添加事件过滤。

func (r *TerraformJobPodFailLogReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&v1.Pod{}).
		WithEventFilter(&predicate.Funcs{
			CreateFunc: func(createEvent event.CreateEvent) bool {
				if !r.filterTerraformJobPod(createEvent.Object) {
					return false
				}
				log.Log.Info("consume terraform-job-pod event. ", "event", "create", "name",
					createEvent.Object.GetName(), "namespace", createEvent.Object.GetNamespace())
				return true
			},
			DeleteFunc: func(deleteEvent event.DeleteEvent) bool {
				if !r.filterTerraformJobPod(deleteEvent.Object) {
					return false
				}
				log.Log.Info("consume terraform-job-pod event. ", "event", "delete", "name",
					deleteEvent.Object.GetName(), "namespace", deleteEvent.Object.GetNamespace())
				return true
			},
			UpdateFunc: func(updateEvent event.UpdateEvent) bool {
				if !r.filterTerraformJobPod(updateEvent.ObjectNew) {
					return false
				}
				log.Log.Info("consume terraform-job-pod event. ", "event", "update", "name",
					updateEvent.ObjectNew.GetName(), "namespace", updateEvent.ObjectNew.GetNamespace())
				return true
			},
			GenericFunc: func(genericEvent event.GenericEvent) bool {
				return false
			},
		}).
		Complete(r)
}

func (r *TerraformJobPodFailLogReconciler) filterTerraformJobPod(eventObj client.Object) bool {
	pod, ok := eventObj.(*v1.Pod)
	if !ok {
		return false
	}
	jobName, ok := pod.Labels["job-name"]
	if !ok || jobName == "" {
		return false
	}
	job := &v12.Job{}
	if err := r.Get(context.TODO(), types.NamespacedName{Namespace: pod.Namespace, Name: jobName}, job); err != nil {
		return false
	}
	if !k8sutil.IsTerraformJob(job) {
		return false
	}
	return true
}

获取pod的terraform-executor的容器日记。

func NewClientSet() (*kubernetes.Clientset, error) {
	localK8sConfig := os.Getenv("KUBECONFIG")
	var restCfg *rest.Config
	var err error
	if localK8sConfig != "" {
		if restCfg, err = clientcmd.BuildConfigFromFlags("", localK8sConfig); err != nil {
			return nil, err
		}
	} else if restCfg, err = rest.InClusterConfig(); err != nil {
		return nil, err
	}
	return kubernetes.NewForConfig(restCfg)
}

func GetPodLog(podNamespaceName types.NamespacedName) (string, error) {
	clientset, err := NewClientSet()
	if err != nil {
		return "", err
	}
	reader, err := clientset.CoreV1().Pods(podNamespaceName.Namespace).GetLogs(podNamespaceName.Name,
		&v1.PodLogOptions{Container: "terraform-executor"}).Stream(context.TODO())
	if err != nil {
		return "", err
	}
	if podLog, err := io.ReadAll(reader); err != nil {
		return "", err
	} else {
		return string(podLog), nil
	}
}

此案例我们选择将获取的pod的日记发送event,关联到job,通过event查看错误日记。完整的controller代码如下。

type TerraformJobPodFailLogReconciler struct {
	client.Client
	Scheme   *runtime.Scheme
	Recorder record.EventRecorder
}

func (r *TerraformJobPodFailLogReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	pod := &v1.Pod{}
	if err := r.Get(ctx, req.NamespacedName, pod); err != nil {
		return ctrl.Result{}, client.IgnoreNotFound(err)
	}

	finalizer := "finalizers.kubernetes.io/pod-log.terraform-daemon"
	if !pod.DeletionTimestamp.IsZero() {
		if controllerutil.ContainsFinalizer(pod, finalizer) {
			r.sendEvent(ctx, pod)
			controllerutil.RemoveFinalizer(pod, finalizer)
			return r.updateResource(ctx, pod)
		}
		return ctrl.Result{}, nil
	}

	if !controllerutil.ContainsFinalizer(pod, finalizer) {
		controllerutil.AddFinalizer(pod, finalizer)
		return r.updateResource(ctx, pod)
	}

	return ctrl.Result{}, nil
}

func (r *TerraformJobPodFailLogReconciler) updateResource(ctx context.Context, resource client.Object) (ctrl.Result, error) {
	if err := r.Update(ctx, resource); err != nil {
		if client.IgnoreNotFound(err) == nil {
			return ctrl.Result{}, nil
		}
		if strings.Contains(err.Error(), "the object has been modified") {
			return ctrl.Result{Requeue: true, RequeueAfter: 3 * time.Second}, nil
		}
		return ctrl.Result{}, client.IgnoreNotFound(err)
	}
	return ctrl.Result{}, nil
}

func (r *TerraformJobPodFailLogReconciler) sendEvent(ctx context.Context, pod *v1.Pod) {
	if pod.Status.Phase == v1.PodSucceeded {
		return
	}

	job := &v12.Job{}
	if err := r.Get(ctx, types.NamespacedName{Namespace: pod.Namespace, Name: getJobName(pod)}, job); err != nil {
		return
	}

	// 获取pod日记
	podLog, err := k8sutil.GetPodLog(types.NamespacedName{Namespace: pod.Namespace, Name: pod.Name})
	if err != nil {
		log.FromContext(ctx).Info(fmt.Sprintf("job: %s pod: %s container: %s get pod log error: %s", getJobName(pod), pod.Name, "terraform-executor", err.Error()))
		return
	}

	// 替换掉特殊字符
	podLog = strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(podLog,
		"[32m+", ""),
		"[0m", ""),
		"[1m", ""),
		"[31m", ""),
		"\u001B", "")

	// 发送事件
	r.Recorder.Event(job, "Warning", "FailedJobExec", podLog)
	log.FromContext(ctx).Info(fmt.Sprintf("success to send event by job %s/%s", job.Namespace, job.Name))
}

func (r *TerraformJobPodFailLogReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&v1.Pod{}).
		WithEventFilter(&predicate.Funcs{
			CreateFunc: func(createEvent event.CreateEvent) bool {
				if !r.filterTerraformJobPod(createEvent.Object) {
					return false
				}
				log.Log.Info("consume terraform-job-pod event. ", "event", "create", "name",
					createEvent.Object.GetName(), "namespace", createEvent.Object.GetNamespace())
				return true
			},
			DeleteFunc: func(deleteEvent event.DeleteEvent) bool {
				if !r.filterTerraformJobPod(deleteEvent.Object) {
					return false
				}
				log.Log.Info("consume terraform-job-pod event. ", "event", "delete", "name",
					deleteEvent.Object.GetName(), "namespace", deleteEvent.Object.GetNamespace())
				return true
			},
			UpdateFunc: func(updateEvent event.UpdateEvent) bool {
				if !r.filterTerraformJobPod(updateEvent.ObjectNew) {
					return false
				}
				log.Log.Info("consume terraform-job-pod event. ", "event", "update", "name",
					updateEvent.ObjectNew.GetName(), "namespace", updateEvent.ObjectNew.GetNamespace())
				return true
			},
			GenericFunc: func(genericEvent event.GenericEvent) bool {
				return false
			},
		}).
		Complete(r)
}

func (r *TerraformJobPodFailLogReconciler) filterTerraformJobPod(eventObj client.Object) bool {
	pod, ok := eventObj.(*v1.Pod)
	if !ok {
		return false
	}
	jobName, ok := pod.Labels["job-name"]
	if !ok || jobName == "" {
		return false
	}
	job := &v12.Job{}
	if err := r.Get(context.TODO(), types.NamespacedName{Namespace: pod.Namespace, Name: jobName}, job); err != nil {
		return false
	}
	if !k8sutil.IsTerraformJob(job) {
		return false
	}
	return true
}

func getJobName(pod *v1.Pod) string {
	return pod.Labels["job-name"]
}

效果如下:

获取job失败的event

#云原生

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

terraform篇03:terraform provider开发避坑指南

Go sdk本地开发调试sdk依赖问题;关于复杂嵌套结构体的schema声明;状态死循环监听,以及terraform命令终止时如何终止死循环;资源创建接口的默认可选字段不填遇到的坑;HCL代码输入变量的复杂校验。

terraform篇02:为私有云开发一个terraform provider

很多企业内部为了不与云厂商绑定,避免上云容易下云难的尴尬,以及企业内部可能也会做私有云,或者封装一个混合云平台,因此不能直接用云厂商提供的provider。

terraform篇01:基础设施即代码,初识terraform,用代码减少沟通成本

通常申请基础设施,我们需要向运维描述我们需要什么基础设施、什么规格,运维根据我们的描述去检查是否已经申请过这样的资源,有就会直接给我们使用基础设施的信息,没有再帮我们申请,然后告诉我们使用基础设施的信息,例如mysql的jdbc和用户名、密码。如果将描述代码化,基础设施的申请自动化,就能实现“基础设施即代码”。而terraform就是实现“将描述代码化”的工具软件。

Operator实战3:Operator开发过程遇到的问题

kubebuilder使用helm代替kustomize;代码改了但似乎没生效-镜像拉取问题; 使用ConfigMap替代Apollo配置中心的最少改动方案;环境变量的注入以及传递;Kubebuilder单测跑不起来;Helm chart和finalizer特性冲突问题。

Operator实战2:实现webhook修改Job的最大重试次数

terraform-controller默认Job会一直重试,导致重复申请资源。

Operator实战1:使用kubebuilder开发一个部署web服务的Operator

举一个非常简单的需求场景,仅用于介绍如何使用kubebuilder开发一个Operator,非真实需求场景。