Skip to content

Commit

Permalink
addrs: ParseRef and ParseTarget support ephemeral resource addresses
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
apparentlymart committed Apr 30, 2024
1 parent 1d7f970 commit be64383
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 11 deletions.
62 changes: 52 additions & 10 deletions internal/addrs/parse_ref.go
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand All @@ -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
Expand Down
98 changes: 98 additions & 0 deletions internal/addrs/parse_ref_test.go
Expand Up @@ -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`,
Expand Down
13 changes: 12 additions & 1 deletion internal/addrs/parse_target.go
Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
}
Expand Down
60 changes: 60 additions & 0 deletions internal/addrs/parse_target_test.go
Expand Up @@ -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{
Expand Down Expand Up @@ -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{
Expand Down
2 changes: 2 additions & 0 deletions internal/addrs/resource.go
Expand Up @@ -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.
Expand Down

0 comments on commit be64383

Please sign in to comment.