From be64383dcb08aad95d7ff3ec17feee6bec95097a Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Tue, 30 Apr 2024 09:42:54 -0700 Subject: [PATCH] addrs: ParseRef and ParseTarget support ephemeral resource addresses This change is not shippable as-is because it changes the interpretation of any reference starting with "ephemeral.", which would previously have referred to a managed resource type belonging to a provider whose local name is "ephemeral". Therefore this initial attempt is only for prototyping purposes and would need to be modified in some way in order to be shippable. It will presumably need some sort of opt-in within the calling module so that the old interpretation can be preserved by default. --- internal/addrs/parse_ref.go | 62 +++++++++++++++--- internal/addrs/parse_ref_test.go | 98 +++++++++++++++++++++++++++++ internal/addrs/parse_target.go | 13 +++- internal/addrs/parse_target_test.go | 60 ++++++++++++++++++ internal/addrs/resource.go | 2 + 5 files changed, 224 insertions(+), 11 deletions(-) diff --git a/internal/addrs/parse_ref.go b/internal/addrs/parse_ref.go index b77b5f869468..2e9b107470d1 100644 --- a/internal/addrs/parse_ref.go +++ b/internal/addrs/parse_ref.go @@ -226,6 +226,19 @@ func parseRef(traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) { remain := traversal[1:] // trim off "data" so we can use our shared resource reference parser return parseResourceRef(DataResourceMode, rootRange, remain) + case "ephemeral": + if len(traversal) < 3 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: `The "ephemeral" object must be followed by two attribute names: the ephemeral resource type and the resource name.`, + Subject: traversal.SourceRange().Ptr(), + }) + return nil, diags + } + remain := traversal[1:] // trim off "ephemeral" so we can use our shared resource reference parser + return parseResourceRef(EphemeralResourceMode, rootRange, remain) + case "resource": // This is an alias for the normal case of just using a managed resource // type as a top-level symbol, which will serve as an escape mechanism @@ -396,13 +409,40 @@ func parseResourceRef(mode ResourceMode, startRange hcl.Range, traversal hcl.Tra case hcl.TraverseAttr: typeName = tt.Name default: - // If it isn't a TraverseRoot then it must be a "data" reference. - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid reference", - Detail: `The "data" object does not support this operation.`, - Subject: traversal[0].SourceRange().Ptr(), - }) + switch mode { + case ManagedResourceMode: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: `The "resource" object does not support this operation.`, + Subject: traversal[0].SourceRange().Ptr(), + }) + case DataResourceMode: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: `The "data" object does not support this operation.`, + Subject: traversal[0].SourceRange().Ptr(), + }) + case EphemeralResourceMode: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: `The "ephemeral" object does not support this operation.`, + Subject: traversal[0].SourceRange().Ptr(), + }) + default: + // Shouldn't get here because the above should be exhaustive for + // all of the resource modes. But we'll still return a + // minimally-passable error message so that the won't totally + // misbehave if we forget to update this in future. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: `The left operand does not support this operation.`, + Subject: traversal[0].SourceRange().Ptr(), + }) + } return nil, diags } @@ -411,14 +451,16 @@ func parseResourceRef(mode ResourceMode, startRange hcl.Range, traversal hcl.Tra var what string switch mode { case DataResourceMode: - what = "data source" + what = "a data source" + case EphemeralResourceMode: + what = "an ephemeral resource type" default: - what = "resource type" + what = "a resource type" } diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid reference", - Detail: fmt.Sprintf(`A reference to a %s must be followed by at least one attribute access, specifying the resource name.`, what), + Detail: fmt.Sprintf(`A reference to %s must be followed by at least one attribute access, specifying the resource name.`, what), Subject: traversal[1].SourceRange().Ptr(), }) return nil, diags diff --git a/internal/addrs/parse_ref_test.go b/internal/addrs/parse_ref_test.go index 1441c64ac96e..cbe40285a61c 100644 --- a/internal/addrs/parse_ref_test.go +++ b/internal/addrs/parse_ref_test.go @@ -363,6 +363,104 @@ func TestParseRef(t *testing.T) { `The "data" object must be followed by two attribute names: the data source type and the resource name.`, }, + // ephemeral + { + `ephemeral.external.foo`, + &Reference{ + Subject: Resource{ + Mode: EphemeralResourceMode, + Type: "external", + Name: "foo", + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 23, Byte: 22}, + }, + }, + ``, + }, + { + `ephemeral.external.foo.bar`, + &Reference{ + Subject: ResourceInstance{ + Resource: Resource{ + Mode: EphemeralResourceMode, + Type: "external", + Name: "foo", + }, + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 23, Byte: 22}, + }, + Remaining: hcl.Traversal{ + hcl.TraverseAttr{ + Name: "bar", + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 23, Byte: 22}, + End: hcl.Pos{Line: 1, Column: 27, Byte: 26}, + }, + }, + }, + }, + ``, + }, + { + `ephemeral.external.foo["baz"].bar`, + &Reference{ + Subject: ResourceInstance{ + Resource: Resource{ + Mode: EphemeralResourceMode, + Type: "external", + Name: "foo", + }, + Key: StringKey("baz"), + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 30, Byte: 29}, + }, + Remaining: hcl.Traversal{ + hcl.TraverseAttr{ + Name: "bar", + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 30, Byte: 29}, + End: hcl.Pos{Line: 1, Column: 34, Byte: 33}, + }, + }, + }, + }, + ``, + }, + { + `ephemeral.external.foo["baz"]`, + &Reference{ + Subject: ResourceInstance{ + Resource: Resource{ + Mode: EphemeralResourceMode, + Type: "external", + Name: "foo", + }, + Key: StringKey("baz"), + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 30, Byte: 29}, + }, + }, + ``, + }, + { + `ephemeral`, + nil, + `The "ephemeral" object must be followed by two attribute names: the ephemeral resource type and the resource name.`, + }, + { + `ephemeral.external`, + nil, + `The "ephemeral" object must be followed by two attribute names: the ephemeral resource type and the resource name.`, + }, + // local { `local.foo`, diff --git a/internal/addrs/parse_target.go b/internal/addrs/parse_target.go index b84515b42aad..b6d59d688ebd 100644 --- a/internal/addrs/parse_target.go +++ b/internal/addrs/parse_target.go @@ -158,9 +158,13 @@ func parseResourceInstanceUnderModule(moduleAddr ModuleInstance, remain hcl.Trav var diags tfdiags.Diagnostics mode := ManagedResourceMode - if remain.RootName() == "data" { + switch remain.RootName() { + case "data": mode = DataResourceMode remain = remain[1:] + case "ephemeral": + mode = EphemeralResourceMode + remain = remain[1:] } if len(remain) < 2 { @@ -195,6 +199,13 @@ func parseResourceInstanceUnderModule(moduleAddr ModuleInstance, remain hcl.Trav Detail: "A data source name is required.", Subject: remain[0].SourceRange().Ptr(), }) + case EphemeralResourceMode: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address", + Detail: "An ephemeral resource type name is required.", + Subject: remain[0].SourceRange().Ptr(), + }) default: panic("unknown mode") } diff --git a/internal/addrs/parse_target_test.go b/internal/addrs/parse_target_test.go index 2b113586e131..68967c6f0d99 100644 --- a/internal/addrs/parse_target_test.go +++ b/internal/addrs/parse_target_test.go @@ -146,6 +146,45 @@ func TestParseTarget(t *testing.T) { }, ``, }, + { + `ephemeral.aws_instance.foo`, + &Target{ + Subject: AbsResource{ + Resource: Resource{ + Mode: EphemeralResourceMode, + Type: "aws_instance", + Name: "foo", + }, + Module: RootModuleInstance, + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 27, Byte: 26}, + }, + }, + ``, + }, + { + `ephemeral.aws_instance.foo[1]`, + &Target{ + Subject: AbsResourceInstance{ + Resource: ResourceInstance{ + Resource: Resource{ + Mode: EphemeralResourceMode, + Type: "aws_instance", + Name: "foo", + }, + Key: IntKey(1), + }, + Module: RootModuleInstance, + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 30, Byte: 29}, + }, + }, + ``, + }, { `module.foo.aws_instance.bar`, &Target{ @@ -252,6 +291,27 @@ func TestParseTarget(t *testing.T) { }, ``, }, + { + `module.foo.module.bar.ephemeral.aws_instance.baz`, + &Target{ + Subject: AbsResource{ + Resource: Resource{ + Mode: EphemeralResourceMode, + Type: "aws_instance", + Name: "baz", + }, + Module: ModuleInstance{ + {Name: "foo"}, + {Name: "bar"}, + }, + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 49, Byte: 48}, + }, + }, + ``, + }, { `module.foo.module.bar[0].data.aws_instance.baz`, &Target{ diff --git a/internal/addrs/resource.go b/internal/addrs/resource.go index 1c81fe5dae2d..6e4d3a8360df 100644 --- a/internal/addrs/resource.go +++ b/internal/addrs/resource.go @@ -27,6 +27,8 @@ func (r Resource) String() string { return fmt.Sprintf("%s.%s", r.Type, r.Name) case DataResourceMode: return fmt.Sprintf("data.%s.%s", r.Type, r.Name) + case EphemeralResourceMode: + return fmt.Sprintf("ephemeral.%s.%s", r.Type, r.Name) default: // Should never happen, but we'll return a string here rather than // crashing just in case it does.