首页 > 其他分享 >给 Helm 提一个 PR,重温开源项目参与过程

给 Helm 提一个 PR,重温开源项目参与过程

时间:2023-07-05 18:34:17浏览次数:42  
标签:PR kind target err Secret patch 开源 Helm

目录

逛社区

酒足饭饱,闲来无事,打开 Helm 项目的 issues 列表,咦,这个 issue 看起来眉清目秀呀:

其实一点都不“眉清目秀”,这段 issue 描述太冗长了,没有任何排版,看起来很累。咱就看关键词吧:helm upgrade,sensitive data,error,好的,这就够了,加在一起就是说当 helm upgrade 命令执行失败的时候,可能回导致敏感数据泄露

盘它!

领任务

尽管已经有人留言认领这个任务,不过看描述似乎他并不会马上开始编码,而是希望进一步沟通方案。好办好办,转给我吧:

倒是挺干脆,分分钟就 assign 给我了。

不过,其实,这会我并不知道怎么修复这个 bug,我也不熟悉 Helm 的源码,外加已经小半年没咋写过 Golang 了,其实心里挺慌。试试看吧,究竟是宝刀未老还是老骥伏枥。

复现 Bug

要修复 Bug,第一步自然是先看能不能稳定复现。外加 issue 描述里其实我看不出来如何复现,也不是太看清楚问题现象是什么,总之,复现问题是当务之急。

用 GPT-4 来准备 Bug 复现材料

  • 我的需求

下面就是想想自己需要啥材料,然后抛给 GPT。或许我可以这样说:

我发现 Helm 在执行 upgrade 的时候,如果出错,会导致 Secret 里的数据被明文打印出来,存在敏感数据泄露风险。现在我想复现这个问题,我设想的步骤是这样的:
1. 构造一个包含 Secret 的正常的 Helm Chart,Secret 里的数据是从 values.yaml 里读取的,values.yaml 中直接使用 base64 编码字符串,而不是在 secret.yaml 中使用 b64enc;
2. 然后执行 helm install,此刻应该没有问题异常;
3. 修改 values.yaml 中的 base64 字符串,让其非法(比如删除一个结尾的“=”);
4. 执行 helm upgrade 命令,看结果是不是存在敏感数据泄露。
请告诉我相关命令和操作等。
  • 将需求告诉 DevChat

  • DevChat 给出的完整回答

首先,我会概括你的需求:

  • 你想要复现一个问题,该问题是在使用 Helm 进行 upgrade 操作时,如果出错,会导致 Secret 中的数据被明文打印出来,存在敏感数据泄露风险。
  • 你已经设想了一些步骤,包括创建一个包含 Secret 的 Helm Chart,然后进行一些操作来触发这个问题。
    下面是我为你准备的相关命令和操作:
  1. 创建一个包含 Secret 的 Helm Chart。首先,你需要创建一个新的 Helm Chart,可以使用以下命令:
helm create mychart

然后,你需要在 mychart/templates 目录下创建一个新的文件 secret.yaml,并添加以下内容:

apiVersion: v1
kind: Secret
metadata:
  name: mysecret
type: Opaque
data:
  mykey: {{ .Values.mysecret.mykey }}

在 mychart/values.yaml 文件中,添加以下内容:

mysecret:
  mykey: "SGVsbG8gd29ybGQ="  # "Hello world" 的 base64 编码
  1. 执行 helm install 命令。在你的 Helm Chart 目录(即 mychart)下,执行以下命令:
helm install myrelease .

此时,应该没有任何问题或异常。

  1. 修改 values.yaml 文件中的 base64 字符串,让其非法。你可以将 values.yaml 文件中的 mykey 值修改为一个非法的 base64 字符串,例如删除一个结尾的“=”:
mysecret:
  mykey: "SGVsbG8gd29ybGQ"  # 非法的 base64 字符串
  1. 执行 helm upgrade 命令。在你的 Helm Chart 目录下,执行以下命令:
helm upgrade myrelease .

此时,你应该能看到错误信息,包括你的 Secret 数据。

以上就是复现你所说问题的步骤。如果你在执行这些步骤时遇到任何问题,或者需要更多的帮助,欢迎随时向我提问。

对着 GPT-4 给的命令复现 Bug

DevChat 诚不我欺啊,一顿复制粘贴敲键盘,最后我看到了这个:

注意这个错误日志里的这部分:

Error: UPGRADE FAILED: cannot patch "mysecret" with kind Secret:  "" is invalid: patch: Invalid value: "{\"apiVersion\":\"v1\",\"data\":{\"mykey\":\"SGVsbG8gd29ybGQ\"}……

这里的 mykey: SGVsbG8gd29ybGQ 看起来似乎不太敏感,不过它其实意味着假如你存了10个密码在 Secret 里,当更新的时候写错了一个,其他正确的9个会被无差别打印出来。这个日志里包含的是一个未脱敏的完整的 Secret 资源实例内容。

定位 Bug

从日志里来看,JSON 部分大概率是 K8s 相关的库返回的内容,Helm 里直接拼接到自己日志里打印出来了。我们需要找到最接近这个“K8s 相关调用”的地方,然后找到这串日志怎么来的,再加一层“脱敏”。

  1. 搜索关键字“UPGRADE FAILED”

太幸运了,只有一个结果(明显是 upgrade.go 这里),那就长刀直入吧:

rel, err := client.RunWithContext(ctx, args[0], ch, vals)
if err != nil {
	return errors.Wrap(err, "UPGRADE FAILED")
}
  1. 继续跟 RunWithContext() 方法里哪里返回了错误日志

进去 RunWithContext() 方法,基本从名字上就能判断是“执行升级过程”的时候出的错,因为一开始的错误日志里有一句“cannot patch "mysecret" with kind Secret”,这显然也不是 Helm 本身的行为,而是某个 K8s 相关的库返回的,因此肯定是开始执行的时候出的错,也就是 performUpgrade() 这个方法返回的 err 里包含了我们所寻找的错误日志。

func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
    //……
	res, err := u.performUpgrade(ctx, currentRelease, upgradedRelease)
	if err != nil {
		return res, err
	}
    //……

	return res, nil
}
  1. 继续跟 performUpgrade()

这个方法有点长,仔细看看,大概率是在结尾的时候 result.e 里包含了我们寻找的 err,而这个 result 是一个 Channel,在 releasingUpgrade 这个协程里完成了数据写入。行,下一步,继续看 releasingUpgrade()

func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedRelease *release.Release) (*release.Release, error) {
    // ……
	go u.releasingUpgrade(rChan, upgradedRelease, current, target, originalRelease)
	go u.handleContext(ctx, doneChan, ctxChan, upgradedRelease)
	select {
	case result := <-rChan:
		return result.r, result.e
	case result := <-ctxChan:
		return result.r, result.e
	}
  1. 继续看 releasingUpgrade()

到这里就差不多了,这里有关键的一行代码:u.cfg.KubeClient.Update(current, target, u.Force)

func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *release.Release, current kube.ResourceList, target kube.ResourceList, originalRelease *release.Release) {
	// ……
	results, err := u.cfg.KubeClient.Update(current, target, u.Force)
	if err != nil {
		u.cfg.recordRelease(originalRelease)
		u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err)
		return
	}
    //……
}

到这里就能猜到很快就应该接近 Helm 和 K8s 相关库的交界点了,“KubeClient”这个名字看起来就不像是 Helm 自身逻辑里的代码。我们继续来看 Update 就行:

  1. 继续跟 Update() 接口

这里比较直观,我们要找到的实现肯定是 client.go 里面那个 Update。

  1. Update() 接口的实现

这个方法看起来也挺复杂,里面藏了一个关键的 updateResource() 函数:

func (c *Client) Update(original, target ResourceList, force bool) (*Result, error) {
	// ……
	err := target.Visit(func(info *resource.Info, err error) error {
		//……
		if err := updateResource(c, info, originalInfo.Object, force); err != nil {
			c.Log("error updating the resource %q:\n\t %v", info.Name, err)
			updateErrors = append(updateErrors, err.Error())
		}
		// ……
	})
    // ……
}
  1. 继续看 updateResource() 函数
func updateResource(c *Client, target *resource.Info, currentObj runtime.Object, force bool) error {
	// ……
	if force {
		// ……
	} else {
		// ……
		// send patch to server
		c.Log("Patch %s %q in namespace %s", kind, target.Name, target.Namespace)
		obj, err = helper.Patch(target.Namespace, target.Name, patchType, patch, nil)
		if err != nil {
			return errors.Wrapf(err, "cannot patch %q with kind %s", target.Name, kind)
		}
	}
	// ……
}

到这里就比较明确了,是 helper.Patch(target.Namespace, target.Name, patchType, patch, nil) 这个方法调用返回了一个包含敏感数据的 err。如果继续看一眼 Patch 方法的定义,就能找到:

这是 k8s.io/cli-runtime 里的代码,已经离开了 Helm 的“管辖范围”。

修复思路

很明显,敏感数据来自于这几行代码:

obj, err = helper.Patch(target.Namespace, target.Name, patchType, patch, nil)
if err != nil {
	return errors.Wrapf(err, "cannot patch %q with kind %s", target.Name, kind)
}

我们希望尽脱敏这些数据,自然是在当前函数/方法内完成这个过程,不能把锅留给调用方(上层函数)。所以下一步就是在 if err != nil { 之后,return 之前加入一个日志过滤函数,在这个函数内实现日志脱敏。

编写代码

  • 增加一个函数来完成脱敏逻辑:
// desensitizeLog replaces the data in a Secret with {"key": "***"}.
// e.g. "data": {"username": "admin", "password": "password"} becomes "data": {"username": "***", "password": "***"}
func desensitizeLog(errLog string) string {
	// Find the start and end index of the JSON string
	start := strings.Index(errLog, "{\\\"apiVersion")
	end := strings.LastIndex(errLog, ",\\\"kind\\\":\\\"Secret\\\"")

	// Extract the JSON string and unescape it
	jsonStr := strings.ReplaceAll(errLog[start:end], "\\\"", "\"")+ "}"

	// Unmarshal the JSON string into a Secret struct
	var secret Secret
	json.Unmarshal([]byte(jsonStr), &secret)

	// Desensitize the data in the Secret struct
	for key := range secret.Data {
		secret.Data[key] = "***"
	}

	// Marshal the Secret struct back into a JSON string
	desensitizedJson, _ := json.Marshal(secret)

	// Escape the JSON string and replace it in the original log
	fixedDesensitizedJson := strings.ReplaceAll(string(desensitizedJson), "\"", "\\\"")
	desensitizedLog := errLog[:start] + fixedDesensitizedJson[:len(fixedDesensitizedJson)-1] + errLog[end:]

	return desensitizedLog
}
  • 把函数调用加到老代码里:
c.Log("Patch %s %q in namespace %s", kind, target.Name, target.Namespace)
obj, err = helper.Patch(target.Namespace, target.Name, patchType, patch, nil)
if err != nil {
	sanitizeLog:=err.Error()
	if kind == "Secret" {
		sanitizeLog = desensitizeLog(err.Error())
	}
	return errors.Wrapf(errors.New(sanitizeLog), "cannot patch %q with kind %s", target.Name, kind)
}

当 kind 为 Secret 的时候,就走一遍脱敏过程。

  • 再加一段 UT 吧:
func TestDesensitizeLog(t *testing.T) {
	// Define a test error log
	errLog := `cannot patch "test-release-secret" with kind Secret:  "" is invalid: 
patch: Invalid value: "{\"apiVersion\":\"v1\",\"data\":{\"secretKey\":\"hello\", \"anotherKey\":\"world\"},\"kind\":\"Secret\",`

	// Call the function
	result := desensitizeLog(errLog)

	// Check if the "data" field has been correctly sanitized
	if !strings.Contains(result, "\\\"secretKey\\\":\\\"***\\\"") ||
		!strings.Contains(result, "\\\"anotherKey\\\":\\\"***\\\"") {
		t.Errorf("The function did not correctly sanitize the error log")
	}
}

测试

UT 需要能通过,这就不用赘述了。UT 过了之后,手动测试下:

酷,看起来挺和谐了。

提 PR

最后一步,提个 PR:

关于如何参与开源项目,如何在 GitHub 上提 PR,我这有一篇非常详细的文章:

ok,不啰嗦了。可能你已经发现,结尾有点仓促,没错,我写到一半的时候,到饭点了,赶紧收个尾,填肚子去咯~

标签:PR,kind,target,err,Secret,patch,开源,Helm
From: https://www.cnblogs.com/daniel-hutao/p/open-a-pr-for-helm.html

相关文章

  • 记录 python request ProxyError报错
    【出自】:https://zhuanlan.zhihu.com/p/350015032  侵删 解决办法:在原报错环境中使用下面命令重装低版本 urllib3:pipinstallurllib3==1.25.11 问题根源先查了一下 urllib3 的更新日志,应该是 1.26.0 的修改导致的:  按照这个更新日志,明明应该是增加了 HT......
  • Github | 制作您的第一个开源合并请求
    Github|制作您的第一个开源合并请求文章目录Github|制作您的第一个开源合并请求1.背景2.前提3.上手贡献开源1.背景开源软件是原始源代码可免费获得并可重新分发和修改的软件。作为一名程序员,我们更感兴趣的是如何为他们的代码库做出贡献。许多新手发现开源是可怕和令人生......
  • springboot修改配置
    springboot修改配置在resources下的Application.properties端口:#服务器端口配置server.port=80logo:#修改bannerspring.main.banner-mode=off 图片:#配置图像spring.banner.image.location=de.jpg日志#日志logging.level.root=error查看.properties可以去spring......
  • Mitmproxy 常规功能介绍
    背景mitmproxy是一个开源的中间人代理工具,用于拦截、修改和观察HTTP/HTTPS流量,并支持扩展功能。它包含三个功能模块:mitmproxy、mitmweb和mitmdump,每个模块提供不同的界面和功能。mitmproxy:提供命令行界面,使用命令行进行操作和监控。mitmweb:提供浏览器界面,通过Web界面进行操作......
  • promethues镜像升级
    下载镜像    dockerpullbitnami/prometheus    dockerrun-p9800:9090323da408bf48        检查镜像服务版本      定制镜像升级版本          1.启动一个老版本镜像       dockerrun323da4......
  • Spring Boot中Service层依赖注入问题随笔
    问题描述: Controller 层方法为 static 静态,引入 Service 层时使用 @Autowired 注解自动装配,Controller层方法里无法调用Service层,于是加static修饰Service层的注入产生错误:调用Service层进行数据库操作时,注入的Service层报空指针异常( NullPointerException )......
  • 4. SpringBoot整合mybatis
    1.回顾Spring整合Mybatis​Spring​整合Mybatis​需要定义很多配置类​SpringConfig​配置类导入JdbcConfig​配置类导入MybatisConfig​配置类@Configuration@ComponentScan("com.itheima")@PropertySource("classpath:jdbc.properties")@Import({Jdbc......
  • Spring Aop 所有需要的Maven依赖
    <dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.9.5</version></dependency>这个依赖是AspectJ的编织器(weaver),它是一个面向切面编程(AOP)的工具,用于在运行时对Java代码进行横切(cross-cutting)操作和增强。As......
  • SpringMVC框架详解:模型+核心组件+实现原理等详解
     MVC模型SpringMVC基于MVC模式,因此理解SpringMVC需要先对MVC模式有所了解。MVC是model、view、和controller的缩写,是软件开发中一种常用的架构模式。MVC各部分根据职责进行分离,使程序的结构更为直观,增加了程序的可扩展性、可维护性、可复用性。可以用如下的图形来......
  • springboot 加载自定义的属性配置文件 或者xml文件
    1、properties user.propertiesname=zhangshanage=18  2、xml Pen1.xml<?xmlversion="1.0"encoding="utf-8"?><!DOCTYPEpropertiesSYSTEM"http://java.sun.com/dtd/properties.dtd"><properties><......