The functionality of the “removed” block in OpenTofu was developed not based on the similarly named, but functionally different Terraform feature, but rather on the already existing “moved” block in both the OpenTofu and Terraform codebase under the MPL-2.0 license. While we do not have information about the HashiCorp development process, it is likely, that much of the code in Terraform also originates from the “moved” block implementation and is, in large parts, not an original work of authorship but rather a derivative work. This is supported by the following comment in the Terraform codebase:
// Like MoveEndpoint, RemoveTarget is a wrapping struct that captures the result
// of decoding an HCL traversal representing a relative path from the current
// module to a removeable object.
To accurately show the origin of the code, in this document we will show the differences and similarities by logical code blocks rather than line by line, which can be misleading since the order of functions and code blocks influences the differential view.
We believe that this is just a case of a misunderstanding where the code came from.
The OpenTofu team never has and will not copy, and never has and will not knowingly accept copies of BUSL-1.1-licensed code into the OpenTofu repository.
On or about January ████, 2024 a community contributor submitted a pull request (see ███████████████████) that was a direct copy of a pull request filed against the Terraform repository (see ███████████████████). The contributor did not have any rights to the pull request against the Terraform repository and the Terraform contributor licensed their contribution under the BUSL-1.1 license, which the OpenTofu team discovered and promptly rejected.
As a follow-up to this event, the OpenTofu core team instituted the policy of the “taint team”. If a pull request mirroring a Terraform feature is submitted by a contributor, one core team member is dedicated as the “taint team” (typically the first team member reviewing the PR). This team member compares the code in the Terraform PR and the OpenTofu PR. If the PR is found to be likely in breach of intellectual property rights, the pull request is closed and the contributor is barred from working on that area of the code in the future. Multiple offenders are banned from contributing to the OpenTofu repository as a whole. Core team members participating in such a review regularly recuse themselves from future work on the reviewed PR because they are unable to provide an unbiased review.
As a result of this process, a core team member flagged the following PR: ███████████████████ as being very similar to the Terraform PR ███████████████████ based on the highly specific and non-trivial go-cty handling code present in the PR. Multiple team members were pulled into the review, and even though unsure, the PR was rejected on the grounds that it may be a copy. The reason for the closure was privately communicated to the contributor.
On or about January ████, 2024 a ████ user highlighted the similarities between the implementation of the “removed” block between OpenTofu and Terraform (see ███████████████████). The OpenTofu team took the allegations of possible copyright infringement by one of its own very seriously and promptly conducted a thorough review of the code also presented in this document. We found no wrongdoing as both implementations are based on the “moved” block rather than on each other. The OpenTofu code is functionally different from the Terraform code, contains more and diverging tests and the similarities are attributable to the “moved” block under the MPL-2.0 license.
Following the events described above, the core team instituted a change to the contribution guidelines presented in the main OpenTofu repository ( https://github.com/opentofu/opentofu/blob/main/CONTRIBUTING.md ) in order to make clear to all contributors that copyright infringement is not acceptable when contributing to OpenTofu (see https://github.com/opentofu/opentofu/pull/1209 ). Specifically, the contribution guide now contains the following section:
A note on copyright
We take copyright and intellectual property very seriously. A few quick rules should help you:
- When you submit a PR, you are responsible for the code in that pull request. You signal your acceptance of the DCO with your sign-off.
- If you include code in your PR that you didn’t write yourself, make sure you have permission from the author. If you have permission, always add the Co-authored-by sign-off to your commits to indicate the author of the code you are adding.
- Be careful about AI coding assistants! Coding assistants based on large language models (LLMs), such as ChatGPT or GitHub Copilot, are awesome tools to help. However, in the specific case of OpenTofu the training data may include the BSL-licensed Terraform. Since the OpenTofu/Terraform codebase is very specific and LLMs don’t have any other training sources, they may emit copyrighted code. Please avoid using LLM-based coding assistants as much as possible.
- When you copy/paste code from within the OpenTofu code, always make it explicit where you copied from. This helps us resolve issues later on.
- Before you copy code from external sources, make sure that the license allows this. Also make sure that any licensing requirements, such as attribution, are met. When in doubt, ask first!
- Specifically, do not copy from the Terraform repository, or any PRs others have filed against that repository. This code is licensed under the BSL, a license which is not compatible with OpenTofu. (You may submit the same PR to both Terraform and OpenTofu as long as you are the author of both.)
Warning
To protect the OpenTofu project from legal issues violating these rules will immediately disqualify your PR from being merged and you from working on that area of the OpenTofu code base in the future. Repeat violations may get you barred from contributing to OpenTofu.
This section is referenced in multiple places, such as in the “I’ve been assigned an issue, now what?” section often linked to new contributors:
Have you read this document? If not, please give it a quick skim, especially the sections about copyright and signoffs.
Additionally, ever since OpenTofu’s inception, contributors are required to sign-off their commits on the repository attesting to their adherence to the DCO ( https://developercertificate.org/ ).
Finally, since March 22nd, 2024 the core team is also in the process of refining a PR ( https://github.com/opentofu/opentofu/pull/1423 ) requiring contributors to not only sign off their commits, but also attest to the fact that any code they have not explicitly written themselves is marked with comments explaining their source.
The OpenTofu team has never and will not knowingly mislabel BUSL-1.1-licensed code as MPL-2.0. It has adequate processes in place to ensure that third party submissions also do not mislabel code with the incorrect license.
OpenTofu and its predecessor, a version of Terraform shortly after 1.5.5, are licensed under the MPL-2.0 license. Much of the original code base contained copyright headers with an SPDX license identifier as follows:
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
It is customary to move code into new files or copy code within the existing project during development and in doing so, creating a derivative work of the original, HashiCorp-owned code under the MPL-2.0 license. If the license header was not added to newly created files, it would likely constitute removal of copyright management information (CMI). Unfortunately, it would not be practical to accurately keep track of which code pieces were originally written or licensed to HashiCorp under their CLA and which ones were purely new creations in OpenTofu. Arguably, OpenTofu in its entirety is a derivative work of the MPL-2.0-licensed version of Terraform. For both potential legal complications and wishing to honor HashiCorps original copyright, the core developer team decided to do so by adding the following headers to all files containing code:
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
The only files that do not fall under this rule are supplemental files that did not originally have copyright headers, such as example code snippets in the documentation or supplemental files that were newly added to OpenTofu only (such as the installation instructions on the website).
In both projects the file names presented in this document are identical or similar. However, these file names originate with the naming convention of the “moved” block, which has the following file names:
We compare the move_statement.go (OpenTofu/Terraform, MPL-2.0), remove_statement.go (OpenTofu, MPL-2.0) and remove_statement.go (Terraform, BUSL-1.1) files code block by code block and remove the comments for readability. Where we have removed code comments in the code analysis below, all code comments differed between HashiCorp’s BUSL-1.1-licensed code and OpenTofu’s MPL-2.0-licensed code.
We start with the move_statement.go (MPL-2.0 license):
type MoveStatement struct {
, To *addrs.MoveEndpointInModule
From.SourceRange
DeclRange tfdiags
bool
Implied }
Comparing to the remove_statement.go in OpenTofu (MPL-2.0 license):
type RemoveStatement struct {
.ConfigRemovable
From addrs.SourceRange
DeclRange tfdiags}
And to remove_statement.go in Terraform (BUSL-1.1 license):
type RemoveStatement struct {
.ConfigMoveable
From addrs
bool
Destroy .SourceRange
DeclRange tfdiags}
Again, we compare the functions to the original
FindMoveStatements
function (MPL-2.0):
// FindMoveStatements recurses through the modules of the given configuration
// and returns a flat set of all "moved" blocks defined within, in a
// deterministic but undefined order.
func FindMoveStatements(rootCfg *configs.Config) []MoveStatement {
return findMoveStatements(rootCfg, nil)
}
Comparing to the OpenTofu implementation (MPL-2.0):
// GetEndpointsToRemove recurses through the modules of the given configuration
// and returns an array of all "removed" addresses within, in a
// deterministic but undefined order.
// We also validate that the removed modules/resources configuration blocks were removed.
func GetEndpointsToRemove(rootCfg *configs.Config) ([]addrs.ConfigRemovable, tfdiags.Diagnostics) {
:= findRemoveStatements(rootCfg, nil)
rm := validateRemoveStatements(rootCfg, rm)
diags := make([]addrs.ConfigRemovable, len(rm))
removedAddresses for i, rs := range rm {
[i] = rs.From
removedAddresses}
return removedAddresses, diags
}
Finally, the Terraform version (BUSL-1.1):
// FindRemoveStatements recurses through the modules of the given configuration
// and returns a set of all "removed" blocks defined within after deduplication
// on the From address.
//
// Error diagnostics are returned if any resource or module targeted by a remove
// block is still defined in configuration.
//
// A "removed" block in a parent module overrides a removed block in a child
// module when both target the same configuration object.
func FindRemoveStatements(rootCfg *configs.Config) (addrs.Map[addrs.ConfigMoveable, RemoveStatement], tfdiags.Diagnostics) {
:= findRemoveStatements(rootCfg, addrs.MakeMap[addrs.ConfigMoveable, RemoveStatement]())
stmts := validateRemoveStatements(rootCfg, stmts)
diags return stmts, diags
}
It is worth noting that function names starting with the word “validate” are customary in Go programming and there are at least 40 such occurrences in the entire codebase, such as ValidateMoves, ValidateMoveStatementGraph, validateProviderConfig, ValidateTargetFile, etc. In other respects, the OpenTofu implementation is functionally dissimilar to the Terraform implementation.
Here we note that upper case and lower case functions are different in the Go programming language and should not be confused. We compare to the findMoveStatements function in the original MPL-2.0 licensed code:
func findMoveStatements(cfg *configs.Config, into []MoveStatement) []MoveStatement {
:= cfg.Path
modAddr for _, mc := range cfg.Module.Moved {
, toAddr := addrs.UnifyMoveEndpoints(modAddr, mc.From, mc.To)
fromAddrif fromAddr == nil || toAddr == nil {
// Invalid combination should've been caught during original
// configuration decoding, in the configs package.
panic(fmt.Sprintf("incompatible move endpoints in %s", mc.DeclRange))
}
= append(into, MoveStatement{
into : fromAddr,
From: toAddr,
To: tfdiags.SourceRangeFromHCL(mc.DeclRange),
DeclRange: false,
Implied})
}
for _, childCfg := range cfg.Children {
= findMoveStatements(childCfg, into)
into }
return into
}
Comparing to the findRemoveStatements in OpenTofu (MPL-2.0):
func findRemoveStatements(cfg *configs.Config, into []*RemoveStatement) []*RemoveStatement {
:= cfg.Path
modAddr
for _, rc := range cfg.Module.Removed {
var removedEndpoint *RemoveStatement
switch FromAddress := rc.From.RelSubject.(type) {
case addrs.ConfigResource:
:= make(addrs.Module, 0, len(modAddr)+len(FromAddress.Module))
absModule = append(absModule, modAddr...)
absModule = append(absModule, FromAddress.Module...)
absModule
var absConfigResource addrs.ConfigRemovable = addrs.ConfigResource{
: FromAddress.Resource,
Resource: absModule,
Module}
= &RemoveStatement{From: absConfigResource, DeclRange: tfdiags.SourceRangeFromHCL(rc.DeclRange)}
removedEndpoint
case addrs.Module:
var absModule = make(addrs.Module, 0, len(modAddr)+len(FromAddress))
= append(absModule, modAddr...)
absModule = append(absModule, FromAddress...)
absModule = &RemoveStatement{From: absModule, DeclRange: tfdiags.SourceRangeFromHCL(rc.DeclRange)}
removedEndpoint
default:
panic(fmt.Sprintf("unhandled address type %T", FromAddress))
}
= append(into, removedEndpoint)
into
}
for _, childCfg := range cfg.Children {
= findRemoveStatements(childCfg, into)
into }
return into
}
Comparing to the Terraform version (BUSL-1.1):
func findRemoveStatements(cfg *configs.Config, into addrs.Map[addrs.ConfigMoveable, RemoveStatement]) addrs.Map[addrs.ConfigMoveable, RemoveStatement] {
for _, mc := range cfg.Module.Removed {
switch mc.From.ObjectKind() {
case addrs.RemoveTargetResource:
:= mc.From.RelSubject.(addrs.ConfigResource)
res := addrs.ConfigResource{
fromAddr : append(cfg.Path, res.Module...),
Module: res.Resource,
Resource}
, ok := into.GetOk(fromAddr)
existingStatementif ok {
if existingResource, ok := existingStatement.From.(addrs.ConfigResource); ok &&
.Equal(fromAddr) {
existingResourcecontinue
}
}
.Put(fromAddr, RemoveStatement{
into: fromAddr,
From: mc.Destroy,
Destroy: tfdiags.SourceRangeFromHCL(mc.DeclRange),
DeclRange})
case addrs.RemoveTargetModule:
:= mc.From.RelSubject.(addrs.Module)
mod := append(cfg.Path, mod...)
absMod
, ok := into.GetOk(mc.From.RelSubject)
existingStatementif ok {
if existingModule, ok := existingStatement.From.(addrs.Module); ok &&
.Equal(absMod) {
existingModulecontinue
}
}
.Put(absMod, RemoveStatement{
into: absMod,
From: mc.Destroy,
Destroy: tfdiags.SourceRangeFromHCL(mc.DeclRange),
DeclRange})
default:
panic("Unsupported remove target kind")
}
}
for _, childCfg := range cfg.Children {
= findRemoveStatements(childCfg, into)
into }
return into
}
Since these blocks are lengthy to read, we will compare the functions line by line:
Moved (OpenTofu, MPL-2.0):
func findMoveStatements(cfg *configs.Config, into []MoveStatement) []MoveStatement {
Removed (OpenTofu, MPL-2.0):
func findRemoveStatements(cfg *configs.Config, into []*RemoveStatement) []*RemoveStatement {
Removed (Terraform, BUSL-1.1):
func findRemoveStatements(cfg *configs.Config, into addrs.Map[addrs.ConfigMoveable, RemoveStatement]) addrs.Map[addrs.ConfigMoveable, RemoveStatement] {
Here we note that configs.Config
is a configuration
structure in the original MPL-2.0-licensed code. The into
parameter has a completely different type in the Terraform version (a
key-value mapping instead of a list), which makes the Terraform variant
function different to the original “moved” block.
The next line is present in both the “moved” and “removed” implementation in OpenTofu (MPL-2.0), but missing from Terraform (BUSL-1.1):
:= cfg.Path modAddr
The next line is an iteration, which has a fixed structure in the Go programming language.
Moved (OpenTofu, MPL-2.0):
for _, mc := range cfg.Module.Moved {
Removed (OpenTofu, MPL-2.0):
for _, rc := range cfg.Module.Removed {
Removed (Terraform, BUSL-1.1):
for _, mc := range cfg.Module.Removed {
The next switch block is not present in the “moved” block, and is substantially dissimilar between OpenTofu and Terraform. We start with the switch statement itself. It is worth noting that “switch” is a language element in Go, not a Terraform-specific name.
OpenTofu (MPL-2.0):
switch FromAddress := rc.From.RelSubject.(type) {
Terraform (BUSL-1.1):
switch mc.From.ObjectKind() {
We note that the type switch statement is customary in Go and appears in the codebase in over 100 places. Specifically, the move_endpoint.go (MPL-2.0) contains a similar switch-case:
switch relAddr := relAddr.(type) {
case ModuleInstance:
// [...]
case ModuleAddrType:
// [...]
default:
// [...]
}
Now we evaluate the individual switch cases with comments omitted for readability as they are completely dissimilar.
OpenTofu (MPL-2.0):
case addrs.ConfigResource:
:= make(addrs.Module, 0, len(modAddr)+len(FromAddress.Module))
absModule = append(absModule, modAddr...)
absModule = append(absModule, FromAddress.Module...)
absModule
var absConfigResource addrs.ConfigRemovable = addrs.ConfigResource{
: FromAddress.Resource,
Resource: absModule,
Module}
= &RemoveStatement{From: absConfigResource, DeclRange: tfdiags.SourceRangeFromHCL(rc.DeclRange)} removedEndpoint
Terraform (BUSL-1.1):
case addrs.RemoveTargetResource:
:= mc.From.RelSubject.(addrs.ConfigResource)
res := addrs.ConfigResource{
fromAddr : append(cfg.Path, res.Module...),
Module: res.Resource,
Resource}
, ok := into.GetOk(fromAddr)
existingStatementif ok {
if existingResource, ok := existingStatement.From.(addrs.ConfigResource); ok &&
.Equal(fromAddr) {
existingResourcecontinue
}
}
.Put(fromAddr, RemoveStatement{
into: fromAddr,
From: mc.Destroy,
Destroy: tfdiags.SourceRangeFromHCL(mc.DeclRange),
DeclRange})
Here it is worth noting that the Terraform (BUSL-1.1) code duplicates
the last section (into.Put
) between the two cases, while
the OpenTofu (MPL-2.0) code takes a different approach and only append
to the “into” variable after the switch statement using a different
method. The OpenTofu (MPL-2.0) method is similar to the one used in the
“moved” implementation (MPL-2.0), but is dissimilar to the
implementation in Terraform (BUSL-1.1), which uses “into.Put” instead of
“append”.
The contents of the item placed into the “into” variable in OpenTofu are, again, substantially similar to the original “moved” version (MPL-2.0) present in move_statement.go:
Moved (MPL-2.0):
= append(into, MoveStatement{
into : fromAddr,
From: toAddr,
To: tfdiags.SourceRangeFromHCL(mc.DeclRange),
DeclRange: false,
Implied})
OpenTofu (MPL-2.0, formatted for readability):
= &RemoveStatement{
removedEndpoint : absConfigResource,
From: tfdiags.SourceRangeFromHCL(rc.DeclRange)
DeclRange}
And further down in the code:
= append(into, removedEndpoint) into
Terraform (BUSL-1.1):
.Put(fromAddr, RemoveStatement{
into: fromAddr,
From: mc.Destroy,
Destroy: tfdiags.SourceRangeFromHCL(mc.DeclRange),
DeclRange})
Going back to the “switch” statement in question, we compare without the dissimilar comments.
OpenTofu (MPL-2.0):
case addrs.Module:
var absModule = make(addrs.Module, 0, len(modAddr)+len(FromAddress))
= append(absModule, modAddr...)
absModule = append(absModule, FromAddress...)
absModule = &RemoveStatement{From: absModule, DeclRange: tfdiags.SourceRangeFromHCL(rc.DeclRange)} removedEndpoint
Terraform (BUSL-1.1):
case addrs.RemoveTargetModule:
:= mc.From.RelSubject.(addrs.Module)
mod := append(cfg.Path, mod...)
absMod
, ok := into.GetOk(mc.From.RelSubject)
existingStatementif ok {
if existingModule, ok := existingStatement.From.(addrs.Module); ok &&
.Equal(absMod) {
existingModulecontinue
}
}
.Put(absMod, RemoveStatement{
into: absMod,
From: mc.Destroy,
Destroy: tfdiags.SourceRangeFromHCL(mc.DeclRange),
DeclRange})
As before, the mechanism of appending to “into” is dissimilar.
The last case of the “switch” block is the default case, which is present in the moved block (MPL-2.0) and similar code is present in over 100 places in the codebase.
Moved (MPL-2.0):
default:
panic(fmt.Sprintf("unsupported address type %T", addr))
OpenTofu (MPL-2.0):
default:
panic(fmt.Sprintf("unhandled address type %T", FromAddress))
Terraform (BUSL-1.1):
default:
panic("Unsupported remove target kind")
Finally, the last block is again almost identical to the original (MPL-2.0) “moved” block.
Moved (MPL-2.0):
for _, childCfg := range cfg.Children {
= findMoveStatements(childCfg, into)
into }
Removed (OpenTofu, MPL-2.0):
for _, childCfg := range cfg.Children {
= findRemoveStatements(childCfg, into)
into }
Removed (Terraform, BUSL-1.1):
for _, childCfg := range cfg.Children {
= findRemoveStatements(childCfg, into)
into }
This function originates in the ValidateMoves
function,
an MPL-2.0-licensed function, but has been heavily refactored in
OpenTofu to accommodate the functionality of the “removed” block. In
OpenTofu, the internal logic is also based on the
findRemoveStatements
function (MPL-2.0) described above.
The code in Terraform (BUSL-1.1) likely originates in the same place. As
before, we present the entire functions and then compare the individual
parts, including any code parts that are equivalent to the
ValidateMoves
function (MPL-2.0). We only include the
relevant parts of the ValidateMoves
function (MPL-2.0) for
brevity.
ValidateMoves (OpenTofu, MPL-2.0):
func ValidateMoves(stmts []MoveStatement, rootCfg *configs.Config, declaredInsts instances.Set) tfdiags.Diagnostics {
// [...]
for _, stmt := range stmts {
// [...]
= diags.Append(&hcl.Diagnostic{
diags : hcl.DiagError,
Severity: "Moved object still exists",
Summary: fmt.Sprintf(
Detail"This statement declares a move from %s, but that %s is still declared%s.\n\nChange your configuration so that this %s will be declared as %s instead.",
, noun, declaredAt, shortNoun, absTo,
absFrom),
: stmt.DeclRange.ToHCL().Ptr(),
Subject})
// [...]
return diags
}
OpenTofu (MPL-2.0):
func validateRemoveStatements(cfg *configs.Config, removeStatements []*RemoveStatement) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
for _, rs := range removeStatements {
:= rs.From
fromAddr if fromAddr == nil {
panic(fmt.Sprintf("incompatible Remove endpoint in %s", rs.DeclRange.ToHCL()))
}
switch fromAddr := fromAddr.(type) {
case addrs.ConfigResource:
:= cfg.Descendent(fromAddr.Module)
moduleConfig if moduleConfig != nil && moduleConfig.Module.ResourceByAddr(fromAddr.Resource) != nil {
= diags.Append(&hcl.Diagnostic{
diags : hcl.DiagError,
Severity: "Removed resource block still exists",
Summary: fmt.Sprintf(
Detail"This statement declares a removal of the resource %s, but this resource block still exists in the configuration. Please remove the resource block.",
,
fromAddr),
: rs.DeclRange.ToHCL().Ptr(),
Subject})
}
case addrs.Module:
if cfg.Descendent(fromAddr) != nil {
= diags.Append(&hcl.Diagnostic{
diags : hcl.DiagError,
Severity: "Removed module block still exists",
Summary: fmt.Sprintf(
Detail"This statement declares a removal of the module %s, but this module block still exists in the configuration. Please remove the module block.",
,
fromAddr),
: rs.DeclRange.ToHCL().Ptr(),
Subject})
}
default:
panic(fmt.Sprintf("incompatible Remove endpoint address type in %s", rs.DeclRange.ToHCL()))
}
}
return diags
}
Terraform (BUSL-1.1):
func validateRemoveStatements(cfg *configs.Config, stmts addrs.Map[addrs.ConfigMoveable, RemoveStatement]) (diags tfdiags.Diagnostics) {
for _, rst := range stmts.Keys() {
switch rst := rst.(type) {
case addrs.ConfigResource:
:= cfg.Descendent(rst.Module)
m if m == nil {
break
}
if r := m.Module.ResourceByAddr(rst.Resource); r != nil {
= diags.Append(&hcl.Diagnostic{
diags : hcl.DiagError,
Severity: "Removed resource still exists",
Summary: fmt.Sprintf("This statement declares that %s was removed, but it is still declared in configuration.", rst),
Detail: r.DeclRange.Ptr(),
Subject})
}
case addrs.Module:
if m := cfg.Descendent(rst); m != nil {
= diags.Append(&hcl.Diagnostic{
diags : hcl.DiagError,
Severity: "Removed module still exists",
Summary: fmt.Sprintf("This statement declares that %s was removed, but it is still declared in configuration.", rst),
Detail: m.CallRange.Ptr(),
Subject})
}
}
}
return diags
}
Since the function is long, again, we will go line by line.
ValidateMoves (MPL-2.0):
func ValidateMoves(stmts []MoveStatement, rootCfg *configs.Config, declaredInsts instances.Set) tfdiags.Diagnostics {
Removed (OpenTofu, MPL-2.0):
func validateRemoveStatements(cfg *configs.Config, removeStatements []*RemoveStatement) tfdiags.Diagnostics {
Removed (Terraform, BUSL-1.1):
func validateRemoveStatements(cfg *configs.Config, stmts addrs.Map[addrs.ConfigMoveable, RemoveStatement]) (diags tfdiags.Diagnostics) {
In both the OpenTofu (MPL-2.0) and Terraform (BUSL-1.1) version, the
arguments and return values are similar to ValidateMoves
(MPL-2.0).
The next line is present only in OpenTofu (MPL-2.0), as the Terraform (BUSL-1.1) variant includes this in the function declaration (an uncommon Go programming technique not used or known by many).
OpenTofu (MPL-2.0):
var diags tfdiags.Diagnostics
The next block, again, is an iteration over all remove statements and the corresponding checks.
OpenTofu (MPL-2.0):
for _, rs := range removeStatements {
Terraform (BUSL-1.1):
for _, rst := range stmts.Keys() {
As noted before, the “for” and “range” keywords are inherent to the Go language and are not specific to Terraform (BUSL-1.1). In fact, this is the most obvious way to write an iteration and writing it differently would make the code unnecessarily complicated and possibly have a performance impact.
The next section is present in OpenTofu (MPL-2.0) and missing in Terraform (BUSL-1.1). It hardens the code against bugs as indicated by the comment:
OpenTofu (MPL-2.0):
:= rs.From
fromAddr if fromAddr == nil {
// Invalid value should've been caught during original
// configuration decoding, in the configs package.
panic(fmt.Sprintf("incompatible Remove endpoint in %s", rs.DeclRange.ToHCL()))
}
The next section performs a switch-case similar to
findRemoveStatements
(MPL-2.0).
OpenTofu (MPL-2.0):
switch fromAddr := fromAddr.(type) {
Terraform (BUSL-1.1):
switch rst := rst.(type) {
Again, we note that “switch” and “(type)” are specific to the Go
language, not Terraform (BUSL-1.1). Next, we compare the cases in the
switch. Here we also include the relevant part from the original
ValidateMoves
function (MPL-2.0):
ValidateMoves (MPL-2.0):
= diags.Append(&hcl.Diagnostic{
diags : hcl.DiagError,
Severity: "Moved object still exists",
Summary: fmt.Sprintf(
Detail"This statement declares a move from %s, but that %s is still declared%s.\n\nChange your configuration so that this %s will be declared as %s instead.",
, noun, declaredAt, shortNoun, absTo,
absFrom),
: stmt.DeclRange.ToHCL().Ptr(),
Subject})
OpenTofu (MPL-2.0):
case addrs.ConfigResource:
:= cfg.Descendent(fromAddr.Module)
moduleConfig if moduleConfig != nil && moduleConfig.Module.ResourceByAddr(fromAddr.Resource) != nil {
= diags.Append(&hcl.Diagnostic{
diags : hcl.DiagError,
Severity: "Removed resource block still exists",
Summary: fmt.Sprintf(
Detail"This statement declares a removal of the resource %s, but this resource block still exists in the configuration. Please remove the resource block.",
,
fromAddr),
: rs.DeclRange.ToHCL().Ptr(),
Subject})
}
Terraform (BUSL-1.1):
case addrs.ConfigResource:
:= cfg.Descendent(rst.Module)
m if m == nil {
break
}
if r := m.Module.ResourceByAddr(rst.Resource); r != nil {
= diags.Append(&hcl.Diagnostic{
diags : hcl.DiagError,
Severity: "Removed resource still exists",
Summary: fmt.Sprintf("This statement declares that %s was removed, but it is still declared in configuration.", rst),
Detail: r.DeclRange.Ptr(),
Subject})
}
Here there are only two similarities. The cfg.Descendent
call is a call to the MPL-2.0-licensed Config
structure
discussed before and is the only practical way to retrieve a descendant
config. The same is true for the call to ResourceByAddr
,
which is the only practical way to retrieve the resource associated with
an address entered by the user. Both implementations check if the module
configuration is nil
(empty value), which is again
necessary and customary to do in the Go programming language. However,
the OpenTofu (MPL-2.0) implementation structures this differently and is
more compact than the Terraform (BUSL-1.1) implementation. A similar
calling structure is also present in transform_diff.go
(MPL-2.0 license):
if t.Config == nil {
return false
}
:= t.Config.DescendentForInstance(addr.Module)
cfg if cfg == nil {
return false
}
:= cfg.Module.ResourceByAddr(addr.ConfigResource().Resource)
res if res == nil {
return false
}
Similar error handling is also present in import.go
(MPL-2.0):
:= config.DescendentForInstance(addr.Module)
targetConfig if targetConfig == nil {
:= addr.Module.String()
modulePath = diags.Append(&hcl.Diagnostic{
diags : hcl.DiagError,
Severity: "Import to non-existent module",
Summary: fmt.Sprintf(
Detail"%s is not defined in the configuration. Please add configuration for this module before importing into it.",
,
modulePath),
})
.showDiagnostics(diags)
creturn 1
}
As discussed before, the diagnostics return block is wide spread in the code base and is not specific to the BUSL-1.1-licensed Terraform code.
The same analysis applies to the second case, which deals with modules.
OpenTofu (MPL-2.0):
if cfg.Descendent(fromAddr) != nil {
= diags.Append(&hcl.Diagnostic{
diags : hcl.DiagError,
Severity: "Removed module block still exists",
Summary: fmt.Sprintf(
Detail"This statement declares a removal of the module %s, but this module block still exists in the configuration. Please remove the module block.",
,
fromAddr),
: rs.DeclRange.ToHCL().Ptr(),
Subject})
}
Terraform (BUSL-1.1):
if m := cfg.Descendent(rst); m != nil {
= diags.Append(&hcl.Diagnostic{
diags : hcl.DiagError,
Severity: "Removed module still exists",
Summary: fmt.Sprintf("This statement declares that %s was removed, but it is still declared in configuration.", rst),
Detail: m.CallRange.Ptr(),
Subject})
}
This section will proceed as before. However, it is worth noting that the BUSL-1.1-licensed Terraform code calls out to the prior art that served as inspiration for this code in the first comment of this file:
// Like MoveEndpoint, RemoveTarget is a wrapping struct that captures the result
// of decoding an HCL traversal representing a relative path from the current
// module to a removeable object.
First, we will inspect the struct itself and then the receiver functions defined on this struct.
Moved (MPL-2.0):
// MoveEndpoint is to AbsMoveable and ConfigMoveable what Target is to
// Targetable: a wrapping struct that captures the result of decoding an HCL
// traversal representing a relative path from the current module to
// a moveable object.
//
// Its name reflects that its primary purpose is for the "from" and "to"
// addresses in a "moved" statement in the configuration, but it's also
// valid to use MoveEndpoint for other similar mechanisms that give
// OpenTofu hints about historical configuration changes that might
// prompt creating a different plan than OpenTofu would by default.
//
// To obtain a full address from a MoveEndpoint you must use
// either the package function UnifyMoveEndpoints (to get an AbsMoveable) or
// the method ConfigMoveable (to get a ConfigMoveable).
type MoveEndpoint struct {
// SourceRange is the location of the physical endpoint address
// in configuration, if this MoveEndpoint was decoded from a
// configuration expresson.
.SourceRange
SourceRange tfdiags
// Internally we (ab)use AbsMoveable as the representation of our
// relative address, even though everywhere else in OpenTofu
// AbsMoveable always represents a fully-absolute address.
// In practice, due to the implementation of ParseMoveEndpoint,
// this is always either a ModuleInstance or an AbsResourceInstance,
// and we only consider the possibility of interpreting it as
// a AbsModuleCall or an AbsResource in UnifyMoveEndpoints.
// This is intentionally unexported to encapsulate this unusual
// meaning of AbsMoveable.
relSubject AbsMoveable}
Removed (OpenTofu, MPL-2.0):
// RemoveEndpoint is to ConfigRemovable what Target is to Targetable:
// a wrapping struct that captures the result of decoding an HCL
// traversal representing a relative path from the current module to
// a removable object. It is very similar to MoveEndpoint.
//
// Its purpose is to represent the "from" address in a "removed" block
// in the configuration.
//
// To obtain a full address from a RemoveEndpoint we need to combine it
// with any ancestor modules in the configuration
type RemoveEndpoint struct {
// SourceRange is the location of the physical endpoint address
// in configuration, if this RemoveEndpoint was decoded from a
// configuration expression.
.SourceRange
SourceRange tfdiags
// the representation of our relative address as a ConfigRemovable
RelSubject ConfigRemovable}
Removed (Terraform, BUSL-1.1):
// Like MoveEndpoint, RemoveTarget is a wrapping struct that captures the result
// of decoding an HCL traversal representing a relative path from the current
// module to a removeable object.
//
// Remove targets are somewhat simpler than move endpoints, in that they deal
// only with resources and modules defined in configuration, not instances of
// those objects as recorded in state. We are therefore able to determine the
// ConfigMoveable up front, since specifying any resource or module instance key
// in a removed block is invalid.
//
// An interesting quirk of RemoveTarget is that RelSubject denotes a
// configuration object that, if the removed block is valid, should no longer
// exist in configuration. This "last known address" is used to locate and delete
// the appropriate state objects, or, in the case in which the user has forgotten
// to remove the object from configuration, to report the address of that block
// in an error diagnostic.
type RemoveTarget struct {
// SourceRange is the location of the target address in configuration.
.SourceRange
SourceRange tfdiags
// RelSubject, like MoveEndpoint's relSubject, abuses an absolute address
// type to represent a relative address.
RelSubject ConfigMoveable}
Note that the ObjectKind
, String
and
Equal
receiver functions present in Terraform (BUSL-1.1)
are not present in OpenTofu (MPL-2.0).
These functions both originate in the ParseMoveEndpoint
(MPL-2.0) function.
ParseMoveEndpoint (MPL-2.0):
// ParseMoveEndpoint attempts to interpret the given traversal as a
// "move endpoint" address, which is a relative path from the module containing
// the traversal to a movable object in either the same module or in some
// child module.
//
// This deals only with the syntactic element of a move endpoint expression
// in configuration. Before the result will be useful you'll need to combine
// it with the address of the module where it was declared in order to get
// an absolute address relative to the root module.
func ParseMoveEndpoint(traversal hcl.Traversal) (*MoveEndpoint, tfdiags.Diagnostics) {
, remain, diags := parseModuleInstancePrefix(traversal)
pathif diags.HasErrors() {
return nil, diags
}
:= tfdiags.SourceRangeFromHCL(traversal.SourceRange())
rng
if len(remain) == 0 {
return &MoveEndpoint{
: path,
relSubject: rng,
SourceRange}, diags
}
, moreDiags := parseResourceInstanceUnderModule(path, remain)
riAddr= diags.Append(moreDiags)
diags if diags.HasErrors() {
return nil, diags
}
return &MoveEndpoint{
: riAddr,
relSubject: rng,
SourceRange}, diags
}
ParseRemoveEndpoint (OpenTofu, MPL-2.0):
// ParseRemoveEndpoint attempts to interpret the given traversal as a
// "remove endpoint" address, which is a relative path from the module containing
// the traversal to a removable object in either the same module or in some
// child module.
//
// This deals only with the syntactic element of a remove endpoint expression
// in configuration. Before the result will be useful you'll need to combine
// it with the address of the module where it was declared in order to get
// an absolute address relative to the root module.
func ParseRemoveEndpoint(traversal hcl.Traversal) (*RemoveEndpoint, tfdiags.Diagnostics) {
, remain, diags := parseModulePrefix(traversal)
pathif diags.HasErrors() {
return nil, diags
}
:= tfdiags.SourceRangeFromHCL(traversal.SourceRange())
rng
if len(remain) == 0 {
return &RemoveEndpoint{
: path,
RelSubject: rng,
SourceRange}, diags
}
, moreDiags := parseResourceUnderModule(path, remain)
riAddr= diags.Append(moreDiags)
diags if diags.HasErrors() {
return nil, diags
}
if riAddr.Resource.Mode == DataResourceMode {
= diags.Append(&hcl.Diagnostic{
diags : hcl.DiagError,
Severity: "Data source address is not allowed",
Summary: "Data sources cannot be destroyed, and therefore, 'removed' blocks are not allowed to target them. To remove data sources from the state, you should remove the data source block from the configuration.",
Detail: traversal.SourceRange().Ptr(),
Subject})
return nil, diags
}
return &RemoveEndpoint{
: riAddr,
RelSubject: rng,
SourceRange}, diags
}
ParseRemoveTarget (Terraform, BUSL-1.1):
func ParseRemoveTarget(traversal hcl.Traversal) (*RemoveTarget, tfdiags.Diagnostics) {
, remain, diags := parseModulePrefix(traversal)
pathif diags.HasErrors() {
return nil, diags
}
:= tfdiags.SourceRangeFromHCL(traversal.SourceRange())
rng
if len(remain) == 0 {
return &RemoveTarget{
: path,
RelSubject: rng,
SourceRange}, diags
}
, moreDiags := parseConfigResourceUnderModule(path, remain)
rAddr= diags.Append(moreDiags)
diags if diags.HasErrors() {
return nil, diags
}
if rAddr.Resource.Mode == DataResourceMode {
= diags.Append(&hcl.Diagnostic{
diags : hcl.DiagError,
Severity: "Data source address not allowed",
Summary: "Data sources are never destroyed, so they are not valid targets of removed blocks. To remove the data source from state, remove the data source block from configuration.",
Detail: rng.ToHCL().Ptr(),
Subject})
}
return &RemoveTarget{
: rAddr,
RelSubject: rng,
SourceRange}, diags
}
The only difference to the MoveEndpoint
in both
implementation is the testing for the presence of data sources. The lack
of this test in OpenTofu (MPL-2.0) was originally raised as part of the
code review process here (
https://github.com/opentofu/opentofu/pull/1158#discussion_r1463578492 )
and subsequently fixed by the developer writing this code in commit
686a809575a3afe3ed6ca6df4d7de825110019b6. As the developer was using
GitHub Copilot at the time, the error message was likely created by the
AI assistant and when prompted, the same AI assistant was producing a
similar error message when testing for this report. However, we do not
have accurate records of this event as we do not require contributors to
screen-record their coding sessions.
The OpenTofu (MPL-2.0) implementation originates in the move_endpoint_test.go (MPL-2.0) file, which contains very similar test cases. It is worth noting that the OpenTofu (MPL-2.0) implementation has 27 test cases, whereas the Terraform (BUSL-1.1) implementation contains only 8.
Since the test definition is very long, we’ll first highlight the
general structure of the TestParseMoveEndpoint
(MPL-2.0)
function, then the TestParseRemoveEndpoint
(OpenTofu,
MPL-2.0) function, and finally the TestParseRemoveTarget
(Terraform BUSL-1.1) function.
TestParseMoveEndpoint
(MPL-2.0):
func TestParseMoveEndpoint(t *testing.T) {
:= []struct {
tests string
Input // funny intermediate subset of AbsMoveable
WantRel AbsMoveable string
WantErr }{
// [...]
}
for _, test := range tests {
.Run(test.Input, func(t *testing.T) {
t, hclDiags := hclsyntax.ParseTraversalAbs([]byte(test.Input), "", hcl.InitialPos)
traversalif hclDiags.HasErrors() {
// We're not trying to test the HCL parser here, so any
// failures at this point are likely to be bugs in the
// test case itself.
.Fatalf("syntax error: %s", hclDiags.Error())
t}
, diags := ParseMoveEndpoint(traversal)
moveEp
switch {
case test.WantErr != "":
if !diags.HasErrors() {
.Fatalf("unexpected success\nwant error: %s", test.WantErr)
t}
:= diags.Err().Error()
gotErr if gotErr != test.WantErr {
.Fatalf("wrong error\ngot: %s\nwant: %s", gotErr, test.WantErr)
t}
default:
if diags.HasErrors() {
.Fatalf("unexpected error: %s", diags.Err().Error())
t}
if diff := cmp.Diff(test.WantRel, moveEp.relSubject); diff != "" {
.Errorf("wrong result\n%s", diff)
t}
}
})
}
}
TestParseRemoveEndpoint
(OpenTofu,
MPL-2.0):
func TestParseRemoveEndpoint(t *testing.T) {
:= []struct {
tests string
Input
WantRel ConfigRemovablestring
WantErr }{
// [...]
}
for _, test := range tests {
.Run(test.Input, func(t *testing.T) {
t, hclDiags := hclsyntax.ParseTraversalAbs([]byte(test.Input), "", hcl.InitialPos)
traversalif hclDiags.HasErrors() {
// We're not trying to test the HCL parser here, so any
// failures at this point are likely to be bugs in the
// test case itself.
.Fatalf("syntax error: %s", hclDiags.Error())
t}
, diags := ParseRemoveEndpoint(traversal)
moveEp
switch {
case test.WantErr != "":
if !diags.HasErrors() {
.Fatalf("unexpected success\nwant error: %s", test.WantErr)
t}
:= diags.Err().Error()
gotErr if gotErr != test.WantErr {
.Fatalf("wrong error\ngot: %s\nwant: %s", gotErr, test.WantErr)
t}
default:
if diags.HasErrors() {
.Fatalf("unexpected error: %s", diags.Err().Error())
t}
if diff := cmp.Diff(test.WantRel, moveEp.RelSubject); diff != "" {
.Errorf("wrong result\n%s", diff)
t}
}
})
}
}
TestParseRemoveTarget
(BUSL-1.1):
func TestParseRemoveTarget(t *testing.T) {
:= []struct {
tests string
Input
Want ConfigMoveablestring
WantErr }{
// [...]
}
for _, test := range tests {
.Run(test.Input, func(t *testing.T) {
t, hclDiags := hclsyntax.ParseTraversalAbs([]byte(test.Input), "", hcl.InitialPos)
traversalif hclDiags.HasErrors() {
// We're not trying to test the HCL parser here, so any
// failures at this point are likely to be bugs in the
// test case itself.
.Fatalf("syntax error: %s", hclDiags.Error())
t}
, diags := ParseRemoveTarget(traversal)
remT
switch {
case test.WantErr != "":
if !diags.HasErrors() {
.Fatalf("unexpected success\nwant error: %s", test.WantErr)
t}
:= diags.Err().Error()
gotErr if gotErr != test.WantErr {
.Fatalf("wrong error\ngot: %s\nwant: %s", gotErr, test.WantErr)
t}
default:
if diags.HasErrors() {
.Fatalf("unexpected error: %s", diags.Err().Error())
t}
if diff := cmp.Diff(test.Want, remT.RelSubject); diff != "" {
.Errorf("wrong result\n%s", diff)
t}
}
})
}
}
Since the test framework is virtually identical to the original, MPL-2.0-licensed code, we will now compare the test cases. We would like to note that the test structure is mandated largely by technical necessity.
Now we will go over the individual test cases, listing every Terraform (BUSL-1.1) test case and attempting to find an OpenTofu (MPL-2.0) equivalent test case, which is not always possible. It is worth noting that these test cases are required by logic, otherwise the code could not be covered adequately.
This case tests a resource without a module, which is the most basic test case.
Moved (MPL-2.0):
{
`foo.bar`,
{
AbsResourceInstance: RootModuleInstance,
Module: ResourceInstance{
Resource: Resource{
Resource: ManagedResourceMode,
Mode: "foo",
Type: "bar",
Name},
: NoKey,
Key},
},
``,
},
Removed (OpenTofu, MPL-2.0):
{
`foo.bar`,
{
ConfigResource: RootModule,
Module: Resource{
Resource
: ManagedResourceMode,
Mode: "foo",
Type: "bar",
Name},
},
``,
},
Removed (Terraform, BUSL-1.1):
{
`test_instance.bar`,
{
ConfigResource: RootModule,
Module: Resource{
Resource: ManagedResourceMode,
Mode: "test_instance",
Type: "bar",
Name},
},
``,
}
This test case tests a resource inside a module. OpenTofu (MPL-2.0) has two test cases to cover this case, whereas Terraform (BUSL-1.1) and the Moved (MPL-2.0) implementation only have one.
Moved (MPL-2.0):
{
`module.foo.bar.baz`,
,
RootModule{
ConfigResource: Module{"foo"},
Module: Resource{
Resource: ManagedResourceMode,
Mode: "bar",
Type: "baz",
Name},
},
}
Removed (OpenTofu, MPL-2.0):
{
`module.boop`,
{"boop"},
Module``,
},
{
`module.boop.foo.bar`,
{
ConfigResource: Module{"boop"},
Module: Resource{
Resource: ManagedResourceMode,
Mode: "foo",
Type: "bar",
Name},
},
``,
}
Removed (Terraform, BUSL-1.1):
{
`module.foo.test_instance.bar`,
{
ConfigResource: []string{"foo"},
Module: Resource{
Resource: ManagedResourceMode,
Mode: "test_instance",
Type: "bar",
Name},
},
``,
}
This test case shows a resource in a module in a module, which is an extension of test case 2.
Removed (OpenTofu, MPL-2.0):
{
`module.boop.module.bip.foo.bar`,
{
ConfigResource: Module{"boop", "bip"},
Module: Resource{
Resource: ManagedResourceMode,
Mode: "foo",
Type: "bar",
Name},
},
``,
}
Removed (Terraform, BUSL-1.1):
{
`module.foo.module.baz.test_instance.bar`,
{
ConfigResource: []string{"foo", "baz"},
Module: Resource{
Resource: ManagedResourceMode,
Mode: "test_instance",
Type: "bar",
Name},
},
``,
}
These test cases test that data sources are not supported. This is
the data source equivalent of case 1 and case 2. There is no equivalent
code in move_endpoint_test.go (MPL-2.0) because the “moved” endpoint
supports data sources. It is worth noting that the error messages are
the exact error messages emitted by the code by
ParseRemoveEndpoint
(OpenTofu, MPL-2.0) /
ParseRemoveEndpoint
(Terraform, BUSL-1.1). The entire test
case is strictly required on a technical level to achieve testing the
code. It is also worth noting that there is no logical equivalent of the
Terraform (BUSL-1.1) case 5.
Removed (OpenTofu, MPL-2.0):
{
`data.foo.bar`,
nil,
`Data source address is not allowed: Data sources cannot be destroyed, and therefore, 'removed' blocks are not allowed to target them. To remove data sources from the state, you should remove the data source block from the configuration.`,
}
Removed (Terraform, BUSL-1.1):
{
`data.test_ds.moo`,
nil,
`Data source address not allowed: Data sources are never destroyed, so they are not valid targets of removed blocks. To remove the data source from state, remove the data source block from configuration.`,
},
{
`module.foo.data.test_ds.noo`,
nil,
`Data source address not allowed: Data sources are never destroyed, so they are not valid targets of removed blocks. To remove the data source from state, remove the data source block from configuration.`,
}
These test cases test numbered instances or modules. Case 7 is not present in OpenTofu (MPL-2.0).
Removed (OpenTofu, MPL-2.0):
{
`foo.bar[0]`,
nil,
`Resource instance address with keys is not allowed: Resource address cannot be a resource instance (e.g. "null_resource.a[0]"), it must be a resource instead (e.g. "null_resource.a").`,
},
// [...]
{
`module.boop.foo.bar[0]`,
nil,
`Resource instance address with keys is not allowed: Resource address cannot be a resource instance (e.g. "null_resource.a[0]"), it must be a resource instead (e.g. "null_resource.a").`,
},
Removed (Terraform, BUSL 1.1):
{
`test_instance.foo[0]`,
nil,
`Resource instance keys not allowed: Resource address must be a resource (e.g. "test_instance.foo"), not a resource instance (e.g. "test_instance.foo[1]").`,
},
{
`module.foo[0].test_instance.bar`,
nil,
`Module instance keys not allowed: Module address must be a module (e.g. "module.foo"), not a module instance (e.g. "module.foo[1]").`,
},
{
`module.foo.test_instance.bar[0]`,
nil,
`Resource instance keys not allowed: Resource address must be a resource (e.g. "test_instance.foo"), not a resource instance (e.g. "test_instance.foo[1]").`,
}
As mentioned before, OpenTofu (MPL-2.0) has 27 test cases, whereas the Terraform (BUSL-1.1) implementation contains only 8. As shown above, some test cases originate with the “moved” block (MPL-2.0), while other test cases are diverging or logically necessary to write tests that cover all desired behavior.
As before, the removed.go file is based on the moved.go (MPL-2.0) file in OpenTofu (MPL-2.0), which appears to be the case in Terraform (BUSL-1.1) as well.
Moved (MPL-2.0):
type Moved struct {
*addrs.MoveEndpoint
From *addrs.MoveEndpoint
To
.Range
DeclRange hcl}
Removed (OpenTofu, MPL-2.0):
type Removed struct {
*addrs.RemoveEndpoint
From
.Range
DeclRange hcl}
Removed (Terraform, BUSL-1.1):
type Removed struct {
*addrs.RemoveTarget
From
bool
Destroy
.Range
DeclRange hcl}
As before, we will first list the entire function and then go line by line.
Moved (MPL-2.0):
func decodeMovedBlock(block *hcl.Block) (*Moved, hcl.Diagnostics) {
var diags hcl.Diagnostics
:= &Moved{
moved : block.DefRange,
DeclRange}
, moreDiags := block.Body.Content(movedBlockSchema)
content= append(diags, moreDiags...)
diags
if attr, exists := content.Attributes["from"]; exists {
, traversalDiags := hcl.AbsTraversalForExpr(attr.Expr)
from= append(diags, traversalDiags...)
diags if !traversalDiags.HasErrors() {
, fromDiags := addrs.ParseMoveEndpoint(from)
from= append(diags, fromDiags.ToHCL()...)
diags .From = from
moved}
}
if attr, exists := content.Attributes["to"]; exists {
, traversalDiags := hcl.AbsTraversalForExpr(attr.Expr)
to= append(diags, traversalDiags...)
diags if !traversalDiags.HasErrors() {
, toDiags := addrs.ParseMoveEndpoint(to)
to= append(diags, toDiags.ToHCL()...)
diags .To = to
moved}
}
// we can only move from a module to a module, resource to resource, etc.
if !diags.HasErrors() {
if !moved.From.MightUnifyWith(moved.To) {
// We can catch some obviously-wrong combinations early here,
// but we still have other dynamic validation to do at runtime.
= diags.Append(&hcl.Diagnostic{
diags : hcl.DiagError,
Severity: "Invalid \"moved\" addresses",
Summary: "The \"from\" and \"to\" addresses must either both refer to resources or both refer to modules.",
Detail: &moved.DeclRange,
Subject})
}
}
return moved, diags
}
Removed (OpenTofu, MPL-2.0):
func decodeRemovedBlock(block *hcl.Block) (*Removed, hcl.Diagnostics) {
var diags hcl.Diagnostics
:= &Removed{
removed : block.DefRange,
DeclRange}
, moreDiags := block.Body.Content(removedBlockSchema)
content= append(diags, moreDiags...)
diags
if attr, exists := content.Attributes["from"]; exists {
, traversalDiags := hcl.AbsTraversalForExpr(attr.Expr)
from= append(diags, traversalDiags...)
diags if !traversalDiags.HasErrors() {
, fromDiags := addrs.ParseRemoveEndpoint(from)
from= append(diags, fromDiags.ToHCL()...)
diags .From = from
removed}
}
return removed, diags
}
Removed (Terraform, BUSL-1.1):
func decodeRemovedBlock(block *hcl.Block) (*Removed, hcl.Diagnostics) {
var diags hcl.Diagnostics
:= &Removed{
removed : block.DefRange,
DeclRange}
, moreDiags := block.Body.Content(removedBlockSchema)
content= append(diags, moreDiags...)
diags
if attr, exists := content.Attributes["from"]; exists {
, traversalDiags := hcl.AbsTraversalForExpr(attr.Expr)
from= append(diags, traversalDiags...)
diags if !traversalDiags.HasErrors() {
, fromDiags := addrs.ParseRemoveTarget(from)
from= append(diags, fromDiags.ToHCL()...)
diags .From = from
removed}
}
.Destroy = true
removed
for _, block := range content.Blocks {
switch block.Type {
case "lifecycle":
, lcDiags := block.Body.Content(removedLifecycleBlockSchema)
lcContent= append(diags, lcDiags...)
diags
if attr, exists := lcContent.Attributes["destroy"]; exists {
:= gohcl.DecodeExpression(attr.Expr, nil, &removed.Destroy)
valDiags = append(diags, valDiags...)
diags }
}
}
return removed, diags
}
Going line by line, we will first examine the function:
Moved (MPL-2.0):
func decodeMovedBlock(block *hcl.Block) (*Moved, hcl.Diagnostics) {
Removed (OpenTofu, MPL-2.0):
func decodeRemovedBlock(block *hcl.Block) (*Removed, hcl.Diagnostics) {
Removed (Terraform, BUSL-1.1):
func decodeRemovedBlock(block *hcl.Block) (*Removed, hcl.Diagnostics) {
The setup code:
Moved (MPL-2.0):
var diags hcl.Diagnostics
:= &Moved{
moved : block.DefRange,
DeclRange}
, moreDiags := block.Body.Content(movedBlockSchema)
content= append(diags, moreDiags...) diags
Removed (OpenTofu, MPL-2.0):
var diags hcl.Diagnostics
:= &Removed{
removed : block.DefRange,
DeclRange}
, moreDiags := block.Body.Content(removedBlockSchema)
content= append(diags, moreDiags...) diags
Removed (Terraform, BUSL-1.1):
var diags hcl.Diagnostics
:= &Removed{
removed : block.DefRange,
DeclRange}
, moreDiags := block.Body.Content(removedBlockSchema)
content= append(diags, moreDiags...) diags
Processing the “from” parameter:
Moved (MPL-2.0):
if attr, exists := content.Attributes["from"]; exists {
, traversalDiags := hcl.AbsTraversalForExpr(attr.Expr)
from= append(diags, traversalDiags...)
diags if !traversalDiags.HasErrors() {
, fromDiags := addrs.ParseMoveEndpoint(from)
from= append(diags, fromDiags.ToHCL()...)
diags .From = from
moved}
}
Removed (OpenTofu, MPL-2.0):
if attr, exists := content.Attributes["from"]; exists {
, traversalDiags := hcl.AbsTraversalForExpr(attr.Expr)
from= append(diags, traversalDiags...)
diags if !traversalDiags.HasErrors() {
, fromDiags := addrs.ParseRemoveEndpoint(from)
from= append(diags, fromDiags.ToHCL()...)
diags .From = from
removed}
}
Removed (Terraform, BUSL-1.1):
if attr, exists := content.Attributes["from"]; exists {
, traversalDiags := hcl.AbsTraversalForExpr(attr.Expr)
from= append(diags, traversalDiags...)
diags if !traversalDiags.HasErrors() {
, fromDiags := addrs.ParseRemoveTarget(from)
from= append(diags, fromDiags.ToHCL()...)
diags .From = from
removed}
}
The subsequent code in the “moved” (MPL-2.0) and the Terraform (BUSL-1.1) implementation is not present in OpenTofu (MPL-2.0) and the function ends with the same return line in all three cases.
As before, the removedBlockSchema
follows the structure
of the movedBlockSchema
.
Moved (MPL-2.0):
var movedBlockSchema = &hcl.BodySchema{
: []hcl.AttributeSchema{
Attributes{
: "from",
Name: true,
Required},
{
: "to",
Name: true,
Required},
},
}
Removed (OpenTofu, MPL-2.0):
var removedBlockSchema = &hcl.BodySchema{
: []hcl.AttributeSchema{
Attributes{
: "from",
Name: true,
Required},
},
}
Removed (Terraform, BUSL-1.1):
var removedBlockSchema = &hcl.BodySchema{
: []hcl.AttributeSchema{
Attributes{
: "from",
Name: true,
Required},
},
: []hcl.BlockHeaderSchema{
Blocks{
: "lifecycle",
Type},
},
}
As with the test cases before, we will first show the structure of the tests and then analyze the test cases individually.
Moved (MPL-2.0):
func TestMovedBlock_decode(t *testing.T) {
:= hcl.Range{
blockRange : "mock.tf",
Filename: hcl.Pos{Line: 3, Column: 12, Byte: 27},
Start: hcl.Pos{Line: 3, Column: 19, Byte: 34},
End}
:= hcltest.MockExprTraversalSrc("test_instance.foo")
foo_expr := hcltest.MockExprTraversalSrc("test_instance.bar")
bar_expr
:= hcltest.MockExprTraversalSrc("test_instance.foo[1]")
foo_index_expr := hcltest.MockExprTraversalSrc("test_instance.bar[\"one\"]")
bar_index_expr
:= hcltest.MockExprTraversalSrc("module.foo")
mod_foo_expr := hcltest.MockExprTraversalSrc("module.bar")
mod_bar_expr
:= map[string]struct {
tests *hcl.Block
input *Moved
want string
err }{
// [...]
}
for name, test := range tests {
.Run(name, func(t *testing.T) {
t, diags := decodeMovedBlock(test.input)
got
if diags.HasErrors() {
if test.err == "" {
.Fatalf("unexpected error: %s", diags.Errs())
t}
if gotErr := diags[0].Summary; gotErr != test.err {
.Errorf("wrong error, got %q, want %q", gotErr, test.err)
t}
} else if test.err != "" {
.Fatal("expected error")
t}
if !cmp.Equal(got, test.want, cmp.AllowUnexported(addrs.MoveEndpoint{})) {
.Fatalf("wrong result: %s", cmp.Diff(got, test.want))
t}
})
}
}
Removed (OpenTofu, MPL-2.0):
func TestRemovedBlock_decode(t *testing.T) {
:= hcl.Range{
blockRange : "mock.tf",
Filename: hcl.Pos{Line: 3, Column: 12, Byte: 27},
Start: hcl.Pos{Line: 3, Column: 19, Byte: 34},
End}
:= hcltest.MockExprTraversalSrc("test_instance.foo")
foo_expr := hcltest.MockExprTraversalSrc("module.foo")
mod_foo_expr := hcltest.MockExprTraversalSrc("test_instance.foo[1]")
foo_index_expr := hcltest.MockExprTraversalSrc("module.boop[1].test_instance.foo")
mod_boop_index_foo_expr := hcltest.MockExprTraversalSrc("data.test_instance.foo")
data_foo_expr
:= map[string]struct {
tests *hcl.Block
input *Removed
want string
err }{
// [...]
}
for name, test := range tests {
.Run(name, func(t *testing.T) {
t, diags := decodeRemovedBlock(test.input)
got
if diags.HasErrors() {
if test.err == "" {
.Fatalf("unexpected error: %s", diags.Errs())
t}
if gotErr := diags[0].Summary; gotErr != test.err {
.Errorf("wrong error, got %q, want %q", gotErr, test.err)
t}
} else if test.err != "" {
.Fatal("expected error")
t}
if !cmp.Equal(got, test.want, cmp.AllowUnexported(addrs.MoveEndpoint{})) {
.Fatalf("wrong result: %s", cmp.Diff(got, test.want))
t}
})
}
}
Removed (Terraform, BUSL-1.1):
func TestRemovedBlock_decode(t *testing.T) {
:= hcl.Range{
blockRange : "mock.tf",
Filename: hcl.Pos{Line: 3, Column: 12, Byte: 27},
Start: hcl.Pos{Line: 3, Column: 19, Byte: 34},
End}
:= hcltest.MockExprTraversalSrc("test_instance.foo")
foo_expr := hcltest.MockExprTraversalSrc("test_instance.foo[1]")
foo_index_expr := hcltest.MockExprTraversalSrc("module.foo")
mod_foo_expr := hcltest.MockExprTraversalSrc("module.foo[1]")
mod_foo_index_expr
:= map[string]struct {
tests *hcl.Block
input *Removed
want string
err }{
// [...]
}
for name, test := range tests {
.Run(name, func(t *testing.T) {
t, diags := decodeRemovedBlock(test.input)
got
if diags.HasErrors() {
if test.err == "" {
.Fatalf("unexpected error: %s", diags.Errs())
t}
if gotErr := diags[0].Summary; gotErr != test.err {
.Errorf("wrong error, got %q, want %q", gotErr, test.err)
t}
} else if test.err != "" {
.Fatal("expected error")
t}
if !cmp.Equal(got, test.want, cmp.AllowUnexported(addrs.MoveEndpoint{})) {
.Fatalf("wrong result: %s", cmp.Diff(got, test.want))
t}
})
}
}
The test structure is similar, but individually different from each other in the variable declaration block:
Moved (MPL-2.0):
:= hcltest.MockExprTraversalSrc("test_instance.foo")
foo_expr := hcltest.MockExprTraversalSrc("test_instance.bar")
bar_expr
:= hcltest.MockExprTraversalSrc("test_instance.foo[1]")
foo_index_expr := hcltest.MockExprTraversalSrc("test_instance.bar[\"one\"]")
bar_index_expr
:= hcltest.MockExprTraversalSrc("module.foo")
mod_foo_expr := hcltest.MockExprTraversalSrc("module.bar") mod_bar_expr
Removed (OpenTofu, MPL-2.0):
:= hcltest.MockExprTraversalSrc("test_instance.foo")
foo_expr := hcltest.MockExprTraversalSrc("module.foo")
mod_foo_expr := hcltest.MockExprTraversalSrc("test_instance.foo[1]")
foo_index_expr := hcltest.MockExprTraversalSrc("module.boop[1].test_instance.foo")
mod_boop_index_foo_expr := hcltest.MockExprTraversalSrc("data.test_instance.foo") data_foo_expr
Removed (Terraform, BUSL-1.1):
:= hcltest.MockExprTraversalSrc("test_instance.foo")
foo_expr := hcltest.MockExprTraversalSrc("test_instance.foo[1]")
foo_index_expr := hcltest.MockExprTraversalSrc("module.foo")
mod_foo_expr := hcltest.MockExprTraversalSrc("module.foo[1]") mod_foo_index_expr
Now we will analyze each test case, numbering them by their location in the Terraform (BUSL-1.1) code and looking for equivalent test cases in the OpenTofu (MPL-2.0) implementation and the “moved” block (MPL-2.0).
These test cases have no equivalent ones in OpenTofu (MPL-2.0) since the “destroy” option is not present in OpenTofu (MPL-2.0):
Removed (Terraform, BUSL-1.1):
"destroy true": {
&hcl.Block{
: "removed",
Type: hcltest.MockBody(&hcl.BodyContent{
Body: hcl.Attributes{
Attributes"from": {
: "from",
Name: foo_expr,
Expr},
},
: hcl.Blocks{
Blocks&hcl.Block{
: "lifecycle",
Type: hcltest.MockBody(&hcl.BodyContent{
Body: hcl.Attributes{
Attributes"destroy": {
: "destroy",
Name: hcltest.MockExprLiteral(cty.BoolVal(true)),
Expr},
},
}),
},
},
}),
: blockRange,
DefRange},
&Removed{
: mustRemoveEndpointFromExpr(foo_expr),
From: true,
Destroy: blockRange,
DeclRange},
``,
},
"destroy false": {
&hcl.Block{
: "removed",
Type: hcltest.MockBody(&hcl.BodyContent{
Body: hcl.Attributes{
Attributes"from": {
: "from",
Name: foo_expr,
Expr},
},
: hcl.Blocks{
Blocks&hcl.Block{
: "lifecycle",
Type: hcltest.MockBody(&hcl.BodyContent{
Body: hcl.Attributes{
Attributes"destroy": {
: "destroy",
Name: hcltest.MockExprLiteral(cty.BoolVal(false)),
Expr},
},
}),
},
},
}),
: blockRange,
DefRange},
&Removed{
: mustRemoveEndpointFromExpr(foo_expr),
From: false,
Destroy: blockRange,
DeclRange},
``,
},
This test case (BUSL-1.1) is functionally equivalent to test case 2 in OpenTofu (MPL-2.0) and test case 3 in the “moved” (MPL-2.0) implementation.
Moved (MPL-2.0):
"modules": {
&hcl.Block{
: "moved",
Type: hcltest.MockBody(&hcl.BodyContent{
Body: hcl.Attributes{
Attributes"from": {
: "from",
Name: mod_foo_expr,
Expr},
"to": {
: "to",
Name: mod_bar_expr,
Expr},
},
}),
: blockRange,
DefRange},
&Moved{
: mustMoveEndpointFromExpr(mod_foo_expr),
From: mustMoveEndpointFromExpr(mod_bar_expr),
To: blockRange,
DeclRange},
``,
},
Removed (OpenTofu, MPL-2.0):
"modules": {
&hcl.Block{
: "removed",
Type: hcltest.MockBody(&hcl.BodyContent{
Body: hcl.Attributes{
Attributes"from": {
: "from",
Name: mod_foo_expr,
Expr},
},
}),
: blockRange,
DefRange},
&Removed{
: mustRemoveEndpointFromExpr(mod_foo_expr),
From: blockRange,
DeclRange},
``,
},
Removed (Terraform, BUSL-1.1):
"modules": {
&hcl.Block{
: "removed",
Type: hcltest.MockBody(&hcl.BodyContent{
Body: hcl.Attributes{
Attributes"from": {
: "from",
Name: mod_foo_expr,
Expr},
},
: hcl.Blocks{
Blocks&hcl.Block{
: "lifecycle",
Type: hcltest.MockBody(&hcl.BodyContent{
Body: hcl.Attributes{
Attributes"destroy": {
: "destroy",
Name: hcltest.MockExprLiteral(cty.BoolVal(true)),
Expr},
},
}),
},
},
}),
: blockRange,
DefRange},
&Removed{
: mustRemoveEndpointFromExpr(mod_foo_expr),
From: true,
Destroy: blockRange,
DeclRange},
``,
},
This test case deals with the lifecycle block, which again is unique to Terraform (BUSL-1.1) and has no equivalent one in OpenTofu (MPL-2.0) or the “moved” block (MPL-2.0).
Removed (Terraform, BUSL-1.1):
// KEM Unspecified behaviour
"no lifecycle block": {
&hcl.Block{
: "removed",
Type: hcltest.MockBody(&hcl.BodyContent{
Body: hcl.Attributes{
Attributes"from": {
: "from",
Name: foo_expr,
Expr},
},
}),
: blockRange,
DefRange},
&Removed{
: mustRemoveEndpointFromExpr(foo_expr),
From: true,
Destroy: blockRange,
DeclRange},
``,
},
This test case (BUSL-1.1) deals with a missing argument. It is functionally equivalent to the OpenTofu (MPL-2.0) test case 3 and test case 4 in the “moved” block (MPL-2.0).
Moved (MPL-2.0):
"error: missing argument": {
&hcl.Block{
: "moved",
Type: hcltest.MockBody(&hcl.BodyContent{
Body: hcl.Attributes{
Attributes"from": {
: "from",
Name: foo_expr,
Expr},
},
}),
: blockRange,
DefRange},
&Moved{
: mustMoveEndpointFromExpr(foo_expr),
From: blockRange,
DeclRange},
"Missing required argument",
},
Removed (OpenTofu, MPL-2.0):
"error: missing argument": {
&hcl.Block{
: "removed",
Type: hcltest.MockBody(&hcl.BodyContent{
Body: hcl.Attributes{},
Attributes}),
: blockRange,
DefRange},
&Removed{
: blockRange,
DeclRange},
"Missing required argument",
},
Removed (Terraform, BUSL-1.1):
"error: missing argument": {
&hcl.Block{
: "removed",
Type: hcltest.MockBody(&hcl.BodyContent{
Body: hcl.Blocks{
Blocks&hcl.Block{
: "lifecycle",
Type: hcltest.MockBody(&hcl.BodyContent{
Body: hcl.Attributes{
Attributes"destroy": {
: "destroy",
Name: hcltest.MockExprLiteral(cty.BoolVal(true)),
Expr},
},
}),
},
},
}),
: blockRange,
DefRange},
&Removed{
: true,
Destroy: blockRange,
DeclRange},
"Missing required argument",
},
This test case (BUSL-1.1) deals with indexed resource instances. It is functionally equivalent to test case 4 in OpenTofu (MPL-2.0) and test case 2 in the “moved” block (MPL-2.0), but with an added error necessary to cover the differing functionality.
Moved (MPL-2.0):
"indexed resources": {
&hcl.Block{
: "moved",
Type: hcltest.MockBody(&hcl.BodyContent{
Body: hcl.Attributes{
Attributes"from": {
: "from",
Name: foo_index_expr,
Expr},
"to": {
: "to",
Name: bar_index_expr,
Expr},
},
}),
: blockRange,
DefRange},
&Moved{
: mustMoveEndpointFromExpr(foo_index_expr),
From: mustMoveEndpointFromExpr(bar_index_expr),
To: blockRange,
DeclRange},
``,
},
Removed (OpenTofu, MPL-2.0):
"error: indexed resources": {
&hcl.Block{
: "removed",
Type: hcltest.MockBody(&hcl.BodyContent{
Body: hcl.Attributes{
Attributes"from": {
: "from",
Name: foo_index_expr,
Expr},
},
}),
: blockRange,
DefRange},
&Removed{
: blockRange,
DeclRange},
"Resource instance address with keys is not allowed",
},
Removed (Terraform, BUSL-1.1):
"error: indexed resource instance": {
&hcl.Block{
: "removed",
Type: hcltest.MockBody(&hcl.BodyContent{
Body: hcl.Attributes{
Attributes"from": {
: "from",
Name: foo_index_expr,
Expr},
},
: hcl.Blocks{
Blocks&hcl.Block{
: "lifecycle",
Type: hcltest.MockBody(&hcl.BodyContent{
Body: hcl.Attributes{
Attributes"destroy": {
: "destroy",
Name: hcltest.MockExprLiteral(cty.BoolVal(true)),
Expr},
},
}),
},
},
}),
: blockRange,
DefRange},
&Removed{
: nil,
From: true,
Destroy: blockRange,
DeclRange},
`Resource instance keys not allowed`,
},
This test case (BUSL-1.1) is similar to test case 6 (BUSL-1.1), but tests indexed modules instead of indexed resources and is necessary to cover the functionality. It is functionally equivalent to test case 5 in OpenTofu (MPL-2.0) which is based on test case 2 in the “moved” block (MPL-2.0).
Moved (MPL-2.0):
"indexed resources": {
&hcl.Block{
: "moved",
Type: hcltest.MockBody(&hcl.BodyContent{
Body: hcl.Attributes{
Attributes"from": {
: "from",
Name: foo_index_expr,
Expr},
"to": {
: "to",
Name: bar_index_expr,
Expr},
},
}),
: blockRange,
DefRange},
&Moved{
: mustMoveEndpointFromExpr(foo_index_expr),
From: mustMoveEndpointFromExpr(bar_index_expr),
To: blockRange,
DeclRange},
``,
},
Removed (OpenTofu, MPL-2.0):
"error: indexed modules": {
&hcl.Block{
: "removed",
Type: hcltest.MockBody(&hcl.BodyContent{
Body: hcl.Attributes{
Attributes"from": {
: "from",
Name: mod_boop_index_foo_expr,
Expr},
},
}),
: blockRange,
DefRange},
&Removed{
: blockRange,
DeclRange},
"Module instance address with keys is not allowed",
},
Removed (Terraform, BUSL-1.1):
"error: indexed module instance": {
&hcl.Block{
: "removed",
Type: hcltest.MockBody(&hcl.BodyContent{
Body: hcl.Attributes{
Attributes"from": {
: "from",
Name: mod_foo_index_expr,
Expr},
},
: hcl.Blocks{
Blocks&hcl.Block{
: "lifecycle",
Type: hcltest.MockBody(&hcl.BodyContent{
Body: hcl.Attributes{
Attributes"destroy": {
: "destroy",
Name: hcltest.MockExprLiteral(cty.BoolVal(true)),
Expr},
},
}),
},
},
}),
: blockRange,
DefRange},
&Removed{
: nil,
From: true,
Destroy: blockRange,
DeclRange},
`Module instance keys not allowed`,
},
This function is identical in all three implementations and has been copied from the “moved” block (MPL-2.0) in both cases.
Moved (MPL-2.0):
func mustMoveEndpointFromExpr(expr hcl.Expression) *addrs.MoveEndpoint {
, hcldiags := hcl.AbsTraversalForExpr(expr)
traversalif hcldiags.HasErrors() {
panic(hcldiags.Errs())
}
, diags := addrs.ParseMoveEndpoint(traversal)
epif diags.HasErrors() {
panic(diags.Err())
}
return ep
}
Removed (OpenTofu, MPL-2.0):
func mustRemoveEndpointFromExpr(expr hcl.Expression) *addrs.RemoveEndpoint {
, hcldiags := hcl.AbsTraversalForExpr(expr)
traversalif hcldiags.HasErrors() {
panic(hcldiags.Errs())
}
, diags := addrs.ParseRemoveEndpoint(traversal)
epif diags.HasErrors() {
panic(diags.Err())
}
return ep
}
Removed (Terraform, BUSL-1.1):
func mustRemoveEndpointFromExpr(expr hcl.Expression) *addrs.RemoveTarget {
, hcldiags := hcl.AbsTraversalForExpr(expr)
traversalif hcldiags.HasErrors() {
panic(hcldiags.Errs())
}
, diags := addrs.ParseRemoveTarget(traversal)
epif diags.HasErrors() {
panic(diags.Err())
}
return ep
}
The allegations of copyright infringement are unsubstantiated as both the OpenTofu (MPL-2.0) and the Terraform (BUSL-1.1) implementations appear to be derivative works of the “moved” block implementation.