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

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

·

6 min read

絮絮叨

话接前回《用 IaC 的方式管理 EC2 实例 - 每天5分钟玩转 GPT 编程系列(1)》,今日个咱继续来看在给真实的开源项目修复 Bug 这个场景中,GPT 能够发挥几成功力。

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

逛社区

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

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

盘它!

领任务

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

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

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

复现 Bug

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

让 DevChat 准备 Bug 复现材料

如果这是你第一次听说 DevChat,来,“辟邪剑谱”接住咯:

  • 我的需求
我发现 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 数据。

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

对着 DevChat 给的命令复现 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 相关调用”的地方,然后找到这串日志怎么来的,再加一层“脱敏”。

翻看源码,可以在 pkg/kube/client.go 源文件中找到一个 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 的“管辖范围”。

修复 Bug

先理思路。

琢磨下怎么修复

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

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 说:
请用 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

  • DevChat 给出了这样的回答

完整代码如下:

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
}

看起来大差不差,不过这两行明显写错了:

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

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

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

要么改成:

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

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

继续让 DevChat 补充一个 UT

  • 新开一个 topic,将当前最新函数作为上下文,要求 DevChat 写一个 UT
请给这个函数写一个 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\":\"***\" 在结果中存在即可。

  • DevChat 给出的结果

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

  • DevChat 给出的完整 UT 代码如下
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,一切顺利

继续让 DevChat 写 Commit Message

  • 先 add 变更

  • 将变更给 DevChat,让它总结

  • 结果是这样的

  • 稍微润色下,加一个 fix 前缀
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 命令,然后将上述消息粘贴进去,最后消息长这样:

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 的完整内容可以在这里看到。

提 PR

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

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

打完收工

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

拓展阅读

相关资源

  • GitHub:github.com/devchat-ai

  • 注册网站:devchat.ai

  • 微信公众号:微信号为“devchat-ai”,名称暂为“思码逸智能编程”

  • 微信群:“思码逸智能编程”公众号内菜单栏点击“微信群”即可收到群二维码

  • Discord:discord.gg/9t3yrbBUXD