# 给 Helm 修复一个 Bug - 每天5分钟玩转 GPT 编程系列(2)


## 絮絮叨

> 注：这一篇文章中 Bug 定位的部分，也就是翻源码找问题的部分单独拆到了[《给 Helm 提一个 PR，重温开源项目参与过程》](https://www.danielhu.cn/open-a-pr-for-helm/)中。同时为了让两篇文章单独成文，也就都加了一些开头结尾，部分内容重复。

话接前回[《用 IaC 的方式管理 EC2 实例 - 每天5分钟玩转 GPT 编程系列(1)》](https://zh.devchat.blog/gpt-in-5min-1-iac-ec2)，今日个咱继续来看在**给真实的开源项目修复 Bug** 这个场景中，GPT 能够发挥几成功力。

> 剧透：今天我要到 Helm 项目里找一个 Bug，然后让 GPT-4 来修复这个 Bug。

## 逛社区

酒足饭饱，闲来无事，打开 Helm 项目的 [issues](https://github.com/helm/helm/issues) 列表，咦，这个 [issue](https://github.com/helm/helm/issues/12176) 看起来眉清目秀呀：

![](1.png)

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

盘它！

## 领任务

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

![](2.png)

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

不过，其实，这会我并不知道怎么修复这个 bug，我也不熟悉 Helm 的源码，外加已经小半年没咋写过 Golang 了，其实心里挺慌。八层把握吧，毕竟手握屠龙刀 GPT-4，开干！

## 复现 Bug

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

### 让 DevChat 准备 Bug 复现材料

> 如果这是你第一次听说 DevChat，来，“辟邪剑谱”接住咯：
> - [《DevChat：将 GPT-4 无缝融入 VS Code，极致提升你的编程体验》](https://zh.devchat.blog/devchat-gpt4-vscode)

- 我的需求

```txt
我发现 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

![](3.png)

- DevChat 给出的完整回答

> 首先，我会概括你的需求：
> 
> - 你想要复现一个问题，该问题是在使用 Helm 进行 upgrade 操作时，如果出错，会导致 Secret 中的数据被明文打印出来，存在敏感数据泄露风险。
> - 你已经设想了一些步骤，包括创建一个包含 Secret 的 Helm Chart，然后进行一些操作来触发这个问题。
> 下面是我为你准备的相关命令和操作：
> 
> 1. 创建一个包含 Secret 的 Helm Chart。首先，你需要创建一个新的 Helm Chart，可以使用以下命令：
> 
> ```bash
> helm create mychart
> ```
> 
> 然后，你需要在 mychart/templates 目录下创建一个新的文件 secret.yaml，并添加以下内容：
> 
> ```yaml
> apiVersion: v1
> kind: Secret
> metadata:
>   name: mysecret
> type: Opaque
> data:
>   mykey: {{ .Values.mysecret.mykey }}
> ```
> 
> 在 mychart/values.yaml 文件中，添加以下内容：
> 
> ```yaml
> mysecret:
>   mykey: "SGVsbG8gd29ybGQ="  # "Hello world" 的 base64 编码
> ```
> 
> 2. 执行 helm install 命令。在你的 Helm Chart 目录（即 mychart）下，执行以下命令：
> 
> ```bash
> helm install myrelease .
> ```
> 
> 此时，应该没有任何问题或异常。
> 
> 3. 修改 values.yaml 文件中的 base64 字符串，让其非法。你可以将 values.yaml 文件中的 mykey 值修改为一个非法的 base64 字符串，例如删除一个结尾的“=”：
> 
> ```yaml
> mysecret:
>   mykey: "SGVsbG8gd29ybGQ"  # 非法的 base64 字符串
> ```
> 
> 4. 执行 helm upgrade 命令。在你的 Helm Chart 目录下，执行以下命令：
> 
> ```bash
> helm upgrade myrelease .
> ```
> 
> 此时，你应该能看到错误信息，包括你的 Secret 数据。
> 
> 以上就是复现你所说问题的步骤。如果你在执行这些步骤时遇到任何问题，或者需要更多的帮助，欢迎随时向我提问。

### 对着 DevChat 给的命令复现 Bug

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

![](4.png)

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

```txt
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 相关调用”的地方，然后找到这串日志怎么来的，再加一层“脱敏”。

翻看源码，可以在 `pkg/kube/client.go` 源文件中找到一个 `updateResource()` 函数，里面有这样一段代码：

```go
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 方法的定义，就能找到：

![](7.png)

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

## 修复 Bug

先理思路。

### 琢磨下怎么修复

很明显，敏感数据来自于这几行代码：

```go
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` 之前加入一个日志过滤函数，在这个函数内实现日志脱敏。

接着又到了和 GPT 聊需求阶段了，秉承着“能聊天就不写代码”的原则，继续祭出 DevChat。

### 让 DevChat 写一个日志脱敏函数

- 尝试这样和 DevChat 说：

```txt
请用 Golang 帮我写一个日志脱敏函数，入参是一个包含敏感数据的字符串，类似这样：

Error: UPGRADE FAILED: cannot patch "mysecret" with kind Secret:  "" is invalid: patch: Invalid value: "{\"apiVersion\":\"v1\",\"data\":{\"mykey\":\"SGVsbG8gd29ybGQ\"},\"kind\":\"Secret\",\"metadata\"……,\"type\":\"Opaque\"}": illegal base64 data at input byte 12

如你所见，这是一个包含了 K8s Secret 资源实例 JSON 字符串的错误日志，其中 data 部分是一个字典，里面的所有 value 都应该被脱敏，用 *** 来打印。

你需要注意以下几点：
1. 你可以先找出从 {\"apiVersion（包含）到 ,\"kind\":\"Secret\"（不包含）之间的部分，，然后去掉\，加上一个}，这样就能得到一个小的 JSON 字符串类似这样：{"apiVersion":"v1","data":{省略}}，进而你可以解析出内部的字典；
3. 当你解析出来这个字典后，就可以循环遍历这个字典，完成 value 的脱敏了；
4. 脱敏完成后，你需要注意先删去刚才2中额外添加到字符串结尾的一个 }，然后重新将这个新的字符串拼接回去，得到脱敏后的新字符串
5. 你可以用中文回答我，但是代码中的注释一律使用英文。
```

- 将需求发给 DevChat

![](8.png)

- DevChat 给出了这样的回答

![](9.png)

完整代码如下：

```go
package main

import (
	"encoding/json"
	"strings"
)

// DesensitizeLog is a function to desensitize sensitive data in log
func DesensitizeLog(log string) string {
	start := strings.Index(log, "{\"apiVersion")
	end := strings.Index(log, ",\"kind\":\"Secret\"")
	if start == -1 || end == -1 {
		return log
	}

	// Extract the JSON string and add a } at the end
	jsonStr := log[start:end] + "}"
	jsonStr = strings.ReplaceAll(jsonStr, "\\\"", "\"")

	// Parse the JSON string into a map
	var data map[string]interface{}
	err := json.Unmarshal([]byte(jsonStr), &data)
	if err != nil {
		return log
	}

	// Desensitize the values in the data map
	if dataMap, ok := data["data"].(map[string]interface{}); ok {
		for k := range dataMap {
			dataMap[k] = "***"
		}
	}

	// Convert the map back to JSON string
	newJsonStr, err := json.Marshal(data)
	if err != nil {
		return log
	}

	// Replace the original JSON string with the new one in the log
	newJsonStr = []byte(strings.ReplaceAll(string(newJsonStr), "\"", "\\\""))
	newLog := log[:start] + string(newJsonStr[:len(newJsonStr)-1]) + log[end:]

	return newLog
}
```

看起来大差不差，不过这两行明显写错了：

```go
	start := strings.Index(log, "{\"apiVersion")
	end := strings.Index(log, ",\"kind\":\"Secret\"")
```

如果在双引号内要表达 `\"`，那得写成 `\\\"` 才行。这两行要么改成：

```go
	start := strings.Index(log, "{\\\"apiVersion")
	end := strings.Index(log, ",\\\"kind\\\":\\\"Secret\\\"")
```

要么改成：

```go
	start := strings.Index(log, `{\"apiVersion`)
	end := strings.Index(log, `,\"kind\":\"Secret\"`)
```

我们将微调后的代码插入 Helm 项目的 `client.go` 中，然后加几行调用逻辑，同时把函数改成小写字母开头，顺带完善下函数注释：

![](10.png)

### 继续让 DevChat 补充一个 UT

- 新开一个 topic，将当前最新函数作为上下文，要求 DevChat 写一个 UT

```txt
请给这个函数写一个 UT，测试字符串是：
Error: UPGRADE FAILED: cannot patch "mysecret" with kind Secret:  "" is invalid: patch: Invalid value: "{\"apiVersion\":\"v1\",\"data\":{\"mykey1\":\"hello\", \"mykey2\":\"world\"},\"kind\":\"Secret\",\"metadata\"……,\"type\":\"Opaque\"}": illegal base64 data at input byte 12

结果中 hello 和 world 都应该被 *** 替换。但是由于字典无序，所以 mykey1 和 mykey2 的顺序不重要，因此你只需要判断 \"mykey1\":\"***\" 和 \"mykey2\":\"***\" 在结果中存在即可。
```

![](11.gif)

- DevChat 给出的结果

看起来符合预期，直接点击 “Insert Code” 插入代码到 `client_test.go` 的结尾

![](12.gif)

- DevChat 给出的完整 UT 代码如下

```go
package kube

import (
	"strings"
	"testing"
)

func TestDesensitizeLog(t *testing.T) {
	log := `Error: UPGRADE FAILED: cannot patch "mysecret" with kind Secret:  "" is invalid: patch: Invalid value: "{\"apiVersion\":\"v1\",\"data\":{\"mykey1\":\"hello\", \"mykey2\":\"world\"},\"kind\":\"Secret\",\"metadata\"……,\"type\":\"Opaque\"}": illegal base64 data at input byte 12`
	expected1 := `\"mykey1\":\"***\"`
	expected2 := `\"mykey2\":\"***\"`

	result := desensitizeLog(log)

	if !strings.Contains(result, expected1) {
		t.Errorf("Expected %s to contain %s", result, expected1)
	}

	if !strings.Contains(result, expected2) {
		t.Errorf("Expected %s to contain %s", result, expected2)
	}
}
```

- 执行 UT，一切顺利

![](13.png)

### 继续让 DevChat 写 Commit Message

- 先 add 变更

![](14.gif)

- 将变更给 DevChat，让它总结

![](15.gif)

- 结果是这样的

![](16.png)

- 稍微润色下，加一个 fix 前缀

```txt
fix: add log sanitization for Secret kind in updateResource

- Added a new function `desensitizeLog` to sanitize logs by replacing sensitive data in a Secret with "***".
- Modified the `updateResource` function to use `desensitizeLog` when the kind is "Secret".
- Added a test case for the `desensitizeLog` function to ensure it works as expected.
```

- 执行 `git commit -s`

由于 DevChat 目前没有直接支持 sign-of，外加这段信息需要修改，所以这时候不能直接点击 DevChat 消息框里的“Commit”按钮，我们执行 `git commit -s` 命令，然后将上述消息粘贴进去，最后消息长这样：

```txt
fix: add log sanitization for Secret kind in updateResource

- Added a new function `desensitizeLog` to sanitize logs by replacing sensitive data in a Secret with "***".
- Modified the `updateResource` function to use `desensitizeLog` when the kind is "Secret".
- Added a test case for the `desensitizeLog` function to ensure it works as expected.

Signed-off-by: Daniel Hu <tao.hu@merico.dev>
```

这个 Commit 的完整内容可以在[这里](https://github.com/helm/helm/pull/12183/commits/309cad9d9fa846a788cbe46cc5d7c9ef91d0cd9b)看到。

## 提 PR

代码写完了，下一步自然是提 PR。在这一步你可以选择先用中文表达你想要写在 PR 里的内容，然后让 DevChat 帮你翻译；也可以直接用英文写，然后让 DevChat 帮你“润色”语法，这个过程就不赘述了。这是我的 PR：

- [Feat: Add Log Sanitization for Secret Kind in 'Helm Upgrade' Command](https://github.com/helm/helm/pull/12183)

关于如何给开源社区提 PR，我在另外一篇文章里有详细介绍：

- [如何参与开源项目 - 细说 GitHub 上的 PR 全过程](https://www.danielhu.cn/open-a-pr-in-github/)

## 打完收工

今天的五分钟超了，收工，下班。

**拓展阅读**

- [DevChat：将 GPT-4 无缝融入 VS Code，极致提升你的编程体验](https://zh.devchat.blog/devchat-gpt4-vscode)
- [用 IaC 的方式管理 EC2 实例 - 每天5分钟玩转 GPT 编程系列(1)](https://zh.devchat.blog/gpt-in-5min-1-iac-ec2)

**相关资源**

- GitHub：https://github.com/devchat-ai
- 注册网站：https://www.devchat.ai
- 微信公众号：微信号为“devchat-ai”，名称暂为“思码逸智能编程”
- 微信群：“思码逸智能编程”公众号内菜单栏点击“微信群”即可收到群二维码
- Discord：https://discord.gg/9t3yrbBUXD

