stacks: ensure consistent sources between removed blocks (#36915)

pull/36930/head
Liam Cervante 1 year ago committed by GitHub
parent dcff9e4d2c
commit 96e50f680f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -76,6 +76,11 @@ type ConfigNode struct {
// Stack is the definition of this node in the stack tree.
Stack *Stack
// Source is the source address of this stack. This is mainly used to
// ensure consistency in places where a stack might be initialised in
// multiple places (like in different source blocks).
Source sourceaddrs.FinalSource
// Children describes all of the embedded stacks nested directly beneath
// this node in the stack tree. The keys match the labels on the "stack"
// blocks in the configuration that [Config.Stack] was built from, and
@ -150,6 +155,7 @@ func loadConfigDir(sourceAddr sourceaddrs.FinalSource, sources *sourcebundle.Bun
ret := &ConfigNode{
Stack: stack,
Source: sourceAddr,
Children: make(map[string]*ConfigNode),
}
for _, call := range stack.EmbeddedStacks {
@ -236,7 +242,7 @@ func loadConfigDir(sourceAddr sourceaddrs.FinalSource, sources *sourcebundle.Bun
Severity: hcl.DiagError,
Summary: "Invalid removed block",
Detail: "The linked removed block was not executed because the `from` attribute of the removed block targets a component or embedded stack within an orphaned embedded stack.\n\nIn order to remove an entire stack, update your removed block to target the entire removed stack itself instead of the specific elements within it.",
Subject: block.SourceAddrRange.ToHCL().Ptr(),
Subject: block.DeclRange.ToHCL().Ptr(),
})
break
}
@ -244,9 +250,21 @@ func loadConfigDir(sourceAddr sourceaddrs.FinalSource, sources *sourcebundle.Bun
if current != nil {
next := target.Item.Name
if _, ok := current.Children[next]; ok {
if childNode, ok := current.Children[next]; ok {
// Then we've already loaded the configuration for this
// stack in the direct stack call.
// stack in the direct stack call or in another removed
// block.
if childNode.Source != block.FinalSourceAddr {
// but apparently the blocks don't agree on what the
// source should be here, so that is an error
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid source address",
Detail: fmt.Sprintf("Cannot use %q as a source address here: the target stack is already initialised with another source %q.", block.FinalSourceAddr, childNode.Source),
Subject: block.SourceAddrRange.ToHCL().Ptr(),
})
}
continue
}
@ -281,7 +299,15 @@ func loadConfigDir(sourceAddr sourceaddrs.FinalSource, sources *sourcebundle.Bun
cmpn.FinalSourceAddr = effectiveSourceAddr
}
for _, blocks := range stack.RemovedComponents.All() {
for addr, blocks := range stack.RemovedComponents.All() {
var source sourceaddrs.FinalSource
if len(addr.Stack) == 0 {
if cmpn, ok := stack.Components[addr.Item.Name]; ok {
source = cmpn.FinalSourceAddr
}
}
for _, rmvd := range blocks {
effectiveSourceAddr, err := resolveFinalSourceAddr(sourceAddr, rmvd.SourceAddr, rmvd.VersionConstraints, sources)
if err != nil {
@ -297,6 +323,17 @@ func loadConfigDir(sourceAddr sourceaddrs.FinalSource, sources *sourcebundle.Bun
continue
}
if source == nil {
source = effectiveSourceAddr
} else if source != effectiveSourceAddr {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid source address",
Detail: fmt.Sprintf("Cannot use %q as a source address here: the target stack is already initialised with another source %q.", effectiveSourceAddr, source),
Subject: rmvd.SourceAddrRange.ToHCL().Ptr(),
})
}
rmvd.FinalSourceAddr = effectiveSourceAddr
}
}

@ -73,6 +73,65 @@ func TestLoadConfigDirErrors(t *testing.T) {
}
}
func TestLoadConfigDirSourceErrors(t *testing.T) {
bundle, err := sourcebundle.OpenDir("testdata/basics-bundle")
if err != nil {
t.Fatal(err)
}
rootAddr := sourceaddrs.MustParseSource("git::https://example.com/errored-sources.git").(sourceaddrs.RemoteSource)
_, gotDiags := LoadConfigDir(rootAddr, bundle)
sort.SliceStable(gotDiags, func(i, j int) bool {
if gotDiags[i].Severity() != gotDiags[j].Severity() {
return gotDiags[i].Severity() < gotDiags[j].Severity()
}
if gotDiags[i].Description().Summary != gotDiags[j].Description().Summary {
return gotDiags[i].Description().Summary < gotDiags[j].Description().Summary
}
return gotDiags[i].Description().Detail < gotDiags[j].Description().Detail
})
wantDiags := tfdiags.Diagnostics{
tfdiags.Sourceless(tfdiags.Error, "Invalid removed block", "The linked removed block was not executed because the `from` attribute of the removed block targets a component or embedded stack within an orphaned embedded stack.\n\nIn order to remove an entire stack, update your removed block to target the entire removed stack itself instead of the specific elements within it."),
tfdiags.Sourceless(tfdiags.Error, "Invalid source address", "Cannot use \"git::https://example.com/errored-sources.git\" as a source address here: the target stack is already initialised with another source \"git::https://example.com/errored-sources.git//subdir\"."),
tfdiags.Sourceless(tfdiags.Error, "Invalid source address", "Cannot use \"git::https://example.com/errored-sources.git//subdir\" as a source address here: the target stack is already initialised with another source \"git::https://example.com/errored-sources.git\"."),
}
count := len(wantDiags)
if len(gotDiags) > count {
count = len(gotDiags)
}
for i := 0; i < count; i++ {
if i >= len(wantDiags) {
t.Errorf("unexpected diagnostic:\n%s", gotDiags[i])
continue
}
if i >= len(gotDiags) {
t.Errorf("missing diagnostic:\n%s", wantDiags[i])
continue
}
got, want := gotDiags[i], wantDiags[i]
if got, want := got.Severity(), want.Severity(); got != want {
t.Errorf("diagnostics[%d] severity\ngot: %s\nwant: %s", i, got, want)
}
if got, want := got.Description().Summary, want.Description().Summary; got != want {
t.Errorf("diagnostics[%d] summary\ngot: %s\nwant: %s", i, got, want)
}
if got, want := got.Description().Detail, want.Description().Detail; got != want {
t.Errorf("diagnostics[%d] detail\ngot: %s\nwant: %s", i, got, want)
}
}
}
func TestLoadConfigDirBasics(t *testing.T) {
bundle, err := sourcebundle.OpenDir("testdata/basics-bundle")
if err != nil {

@ -0,0 +1,17 @@
variable "name" {
type = string
}
resource "null_resource" "example" {
triggers = {
name = var.name
}
}
output "greeting" {
value = "Hello, ${var.name}!"
}
output "resource_id" {
value = null_resource.example.id
}

@ -0,0 +1,53 @@
required_providers {
null = {
source = "hashicorp/null"
version = "3.2.1"
}
}
provider "null" "a" {}
removed {
from = stack.a.component.a // bad, stack.a is undefined so this is orphaned
source = "./"
providers = {
null = provider.null.a
}
}
removed {
from = stack.a.stack.b // bad, stack.a is undefined so this is orphaned
source = "./subdir"
}
removed {
from = stack.b["a"]
source = "./subdir"
}
removed {
from = stack.b["b"]
source = "./" // bad, the sources should be the same for stack.b
}
removed {
from = stack.a.component.b["a"]
source = "./"
providers = {
null = provider.null.a
}
}
removed {
from = stack.a.component.b["b"] // bad, the sources should be the same for component.b
source = "./subdir"
providers = {
null = provider.null.a
}
}

@ -0,0 +1,20 @@
required_providers {
null = {
source = "hashicorp/null"
version = "3.2.1"
}
}
provider "null" "a" {}
component "a" {
source = "../"
inputs = {
name = var.name
}
providers = {
null = provider.null.a
}
}

@ -15,6 +15,11 @@
"source": "git::https://example.com/errored.git",
"local": "errored",
"meta": {}
},
{
"source": "git::https://example.com/errored-sources.git",
"local": "errored-sources",
"meta": {}
}
],
"registry": [

Loading…
Cancel
Save