跳至主要内容

我们在开发 OpenTofu 新测试功能期间的收获

Eran Elbaz
Arel Rabinowitz
What We Learned While Working on OpenTofu's New Test Feature

加入一个新分支的项目可能是一项艰巨的任务,甚至可能令人望而生畏!现在,想象一下加入一个拥有九年历史的遗留代码库的生产就绪型项目。更重要的是,你的任务是将该项目的一个新功能从实验阶段转变为生产就绪状态。

这就是我们在 OpenTofu 测试功能方面所经历的旅程。

这是一个了解 OpenTofu 幕后工作情况的有趣介绍,因为我们正在处理自己的管道。在这种情况下,我们正在获取原始的实验代码,并将其转变为能够与 (甚至可能超越) Terraform v1.6.0 竞争的东西,并使用直接替换方案 OpenTofu v1.6.alpha。

功能

作为 HashiCorp Terraform 许可证变更 的一部分,我们加入了 OpenTofu 计划。我们的首要任务之一是使 OpenTofu 与即将推出的 Terraform 1.6.0 保持同步,这意味着要将测试功能从实验阶段转变为生产就绪状态。

在分支创建时,测试功能已存在于代码库中。但是,它仍处于实验状态,没有任何关于功能如何工作的文档,并且对测试功能本身的功能没有太多测试覆盖。

这意味着我们必须弄清楚看似处于开发中的测试功能的目的,这种方法的优缺点,并找出使该功能走出“开发地狱”所需的缺失部分。

我们首先通过执行一些不同的操作来映射功能。

首先,我们阅读了之前的测试以了解该功能在不同场景下的行为。接下来,我们仔细阅读了其代码以了解其内部运作机制。然后,我们自己尝试了一下,以体验新的测试功能的使用感受。

这些都很重要,因为您不希望只是跳进去并根据您认为它应该是什么样子开始更改代码。您需要了解原始实现者的架构和意图,以便您编写的代码对其进行补充,而不是与之对抗。

经过这些努力,我们充分掌握了测试功能。它是一个构建的框架,旨在帮助用户以端到端的方式(每个模块)测试其模块中的配置。它确保模块在常见情况下按预期工作,同时以可预测且安全的方式响应故障情况,例如错误配置。

测试功能引入了 *.tftest.hcl 文件。这些文件 1) 描述您的测试套件,以及 2) 支持 HCL 块的特定子集。其中最重要的块是新的 run 块,它构成一个测试运行。

执行 tofu test 时,每个 run 块在幕后执行 tofu plantofu apply,使用为测试指定的配置运行您的模块,并在您的云中实际创建资源(在 apply 的情况下)。

每次运行后,它都会执行验证;也就是说,它确保 1) 所有断言都通过,2) 没有检查失败,以及 3) plan/apply 已成功完成。

在 tofu 完成所有运行和测试后,它会尝试销毁作为该 tofu test 运行的一部分而创建的所有资源。

我们是如何处理的?

在我们的初始测试和代码阅读过程中,我们列出了代码库中未经测试的功能行为列表,以及我们认为可能无法正常工作的行为列表。

我们主要依靠我们阅读的代码以及我们对遗留 Terraform 和先前问题的了解。对于其中大部分,我们最终创建了 拉取请求,添加测试覆盖率,或实际修复错误。

在我们测试该功能期间,我们遇到了一些错误,并提出了一些建议,以在预分叉代码库中已有的基础上实际改进该功能。以下是一些我们最终修复的此类错误的示例。

错误 1:run 块中的敏感值

**错误描述:**当在 run 块的断言中评估敏感值时,tofu test 会崩溃。

在我们进行手动 QA 场景测试时,我们发现运行 tofu test 在某些情况下可能会导致程序崩溃。具体来说,当配置包含一个具有断言的 run 块,并且该断言本身依赖于敏感值时,就会发生这种情况(请参阅下面的代码示例以了解 main.tftest.hcl)。

这不是理想的情况,因为 tofu test 中的崩溃意味着您的云中现在存在一些资源,而 tofu 对这些资源没有任何记录。

您可以通过运行以下配置来实现此崩溃。

main.tf

代码块
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "5.14.0"
}
}
}

resource "aws_secretsmanager_secret" "my_secret" {
name = "my_secret"
}

resource "aws_secretsmanager_secret_version" "my_secret_version" {
secret_id = aws_secretsmanager_secret.my_secret.id
secret_string = "secret_value"
}

main.tftest.hcl

代码块
run "secret_test" {
assert {
condition = aws_secretsmanager_secret_version.my_secret_version.secret_string == "secret_value"
error_message = "bad secret"
}
}

使用 tofu test 运行此配置实际上会导致 tofu 崩溃。aws_secretsmanager_secret_version.my_secret_version.secret_string 是一个敏感值,OpenTofu 在尝试评估其是 true 还是 false 时崩溃了。(您可以关注 GitHub 上的讨论)。

调试这个问题很简单,只需在调试器中运行 CLI(使用 debug-opentofu 脚本,该脚本使用 delve 进行调试),然后跟踪代码和崩溃堆栈跟踪以找出问题根源。最后,为了找出如何解决问题,我们检查了代码库中如何为其他类型的条件解决了此问题。

问题源于 HCL 中值评估的工作方式,使用 go-cty。我们不会详细介绍此评估的工作原理,但某些值可以使用其他信息进行“标记”(例如将值标记为敏感),并且某些操作不能对标记的值进行,从而导致此恐慌。

这是故意的,以确保代码作者始终显式处理这些标记,而不是依赖某些(可能不需要的)隐式行为。

在这种情况下,在检查布尔值之前,简单地取消标记该值是可行的方法:resultVal, _ = resultVal.Unmark()。您可以在此处查看完整代码。

有趣的是,此问题之前在遗留代码库中也曾发生在其他此类条件下,例如preconditionvariable的自定义验证规则,它们具有类似的行为。

错误 2:空输出引用

错误描述:当引用null输出时,tofu test会崩溃。

与之前的错误类似,我们回顾了我们的手动QA场景,并发现了一个tofu test崩溃的场景:当run条件引用一个值为nulloutput时。

您可以通过运行以下配置来实现此崩溃(更多信息此处

main.tftest.hcl

代码块
output "my_output" {
value = null
}


run "test_run" {
assert {
condition = output.my_output != "something"
error_message = "good"
}
}

对于这个错误,经过一些调试工作后,我们发现这是由于tofu没有将null输出序列化为实际输出。例如,它们不会作为状态中的输出出现。

但是,对于tofu test功能,断言必须将这些null输出评估为具有nil值,即使这些输出未被序列化。

代码如下所示

代码块
output := d.Evaluator.State.OutputValue(addr.Absolute(d.ModulePath))
val := output.Value
if val == cty.NilVal {
// Not evaluated yet?
val = cty.DynamicVal
}

if output.Sensitive {
val = val.Mark(marks.Sensitive)
}

return val, diags

如果在地址中未找到输出,则OutputValue返回nil,包括输出值为null且因此未序列化的这种情况。因此,对于修复,我们确保在这种情况下返回nil

代码块
if output == nil {
return cty.NilVal, diags
}

您可以在此处查看完整代码。

其他测试功能改进和建议

除了这些崩溃修复之外,我们发现它在测试覆盖率方面大多存在不足(这很有道理,它仍然处于alpha阶段)。对于测试功能,我们发现许多场景没有被任何类型的测试覆盖,并且错误修复也常常缺少测试覆盖率。

因此,我们向测试功能的集成测试套件中添加了许多新的测试用例。我们认为,对于一个将被广泛使用的开源项目来说,更高的测试覆盖率至关重要,考虑到其大部分现有代码库已经相当古老。有关这些测试用例的更多信息,请参见此处

此外,我们还开始为稳定该功能的建议创建问题。例如,在tofu test运行结束时,资源清理期间发生的故障可能会导致失败,从而导致资源仍然存在于您的云提供商中。如果发生这种情况,OpenTofu只会列出(以HCL格式)未正确删除的资源的名称。

这可能不够好,因为如果没有更多信息,以后在您的云提供商中找到这些资源可能会非常困难。因此,我们建议至少打印出这些资源的ID,以便您可以在云提供商中更轻松地找到它们以进行删除。

我们的收获

这篇博文介绍了我们在开发和故障排除OpenTofu的新测试功能时学到的知识。我们使用代码调试和黑盒测试来构建OpenTofu alpha版本的该功能的功能。

通过这次学习中开发功能代码并使其稳定的冒险经历,我们对代码库以及如何调试它有了更深入的了解。除此之外,我们还学会了仅通过阅读代码和试用它来完全构建功能规范。

这也让我们更加认识到测试对于这种规模的开源项目的重要性(对于文档和确保功能长期有效),我们打算在开发过程中增加项目的测试覆盖率。

当然,这只是投入工作的一个小例子,但它让您了解了我们如何为OpenTofu创建新功能。它不仅将与开源Terraform的过去功能保持一致,还将与更新的功能保持同步,甚至超越仅仅作为Terraform的直接替换,开发其自身的功能。

请查看OpenTofu 1.6.alpha版本,以及我们关于如何开始使用和安装OpenTofu的博文。