Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Ephemeral Resources prototype #35078

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ae365bb
providers: New Interface methods for ephemeral resource types
apparentlymart Apr 29, 2024
a44ff40
addrs: EphemeralResourceMode
apparentlymart Apr 29, 2024
2ab614c
configs: Experimental support for ephemeral resources
apparentlymart Apr 29, 2024
bf8c419
terraform provider: terraform_random_number ephemeral resource type
apparentlymart Apr 29, 2024
b7b8a4c
addrs: ParseRef and ParseTarget support ephemeral resource addresses
apparentlymart Apr 30, 2024
7f71696
terraform: Add ephemeral resources to the graph, and validate refs
apparentlymart Apr 30, 2024
ed135c7
lang: Basic awareness of ephemeral resource evaluation
apparentlymart Apr 30, 2024
c9c4e9b
terraform: Don't panic when visiting ephemeral resource nodes
apparentlymart Apr 30, 2024
8bb989b
terraform: Graph nodes for closing ephemeral resource instances
apparentlymart May 1, 2024
12d066c
resources/ephemeral: A place to track ephemeral resource instances
apparentlymart May 2, 2024
3273bff
terraform: Plumb in an ephemeral.Resources object for each graph walk
apparentlymart May 3, 2024
cd50bfa
terraform: "Close" the graph walker when a graph walker is complete
apparentlymart May 3, 2024
ff85c35
terraform: Open and close ephemeral resource instances during graph walk
apparentlymart May 3, 2024
dc92244
terraform: Close provider after ephemeral resources closed
apparentlymart May 6, 2024
e2e82be
terraform: Use GraphNodeReferencer directly for ephemeral resource an…
apparentlymart May 6, 2024
f911734
terraform: Expression evaluator can deal with ephemeral resource refs
apparentlymart May 6, 2024
c8f8563
terraform: Never prune "unused" ephemeral resource nodes
apparentlymart May 7, 2024
80c2ef1
plans+states: Reject attempts to persist ephemeral resources
apparentlymart May 7, 2024
ee08bab
terraform: Don't try to write ephemeral resources to plan or state
apparentlymart May 7, 2024
b8fe93e
builtin/providers/terraform: Prepare for more ephemeral resource types
apparentlymart May 10, 2024
a268b25
terraform: Better error message for inconsistent ephemeral resource r…
apparentlymart May 10, 2024
9ac6952
terraform: Ephemeral resource close comes after provider close
apparentlymart May 11, 2024
41fa1e1
builtin/providers/terraform: terraform_ssh_tunnels ephemeral resource…
apparentlymart May 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 52 additions & 10 deletions internal/addrs/parse_ref.go
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
15 changes: 13 additions & 2 deletions internal/addrs/parse_target.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,14 @@ 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:]
} else if remain.RootName() == "resource" {
case "ephemeral":
mode = EphemeralResourceMode
remain = remain[1:]
case "resource":
// Starting a resource address with "resource" is optional, so we'll
// just ignore it.
remain = remain[1:]
Expand Down Expand Up @@ -200,6 +204,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
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,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 @@ -271,6 +310,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
30 changes: 30 additions & 0 deletions internal/addrs/resource.go
Original file line number Diff line number Diff line change
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 Expand Up @@ -505,8 +507,36 @@ const (
// DataResourceMode indicates a data resource, as defined by
// "data" blocks in configuration.
DataResourceMode ResourceMode = 'D'

// EphemeralResourceMode indicates an ephemeral resource, as defined by
// "ephemeral" blocks in configuration.
EphemeralResourceMode ResourceMode = 'E'
)

// PersistsBetweenRounds returns true only if resource instances of this mode
// persist in the Terraform state from one plan/apply round to the next.
func (m ResourceMode) PersistsBetweenRounds() bool {
switch m {
case EphemeralResourceMode:
return false
default:
return true
}
}

// PersistsPlanToApply returns true only if resource instances of this mode
// can have planned actions that are decided during the plan phase and then
// carried out during the apply phase, meaning that they must be recorded
// as part of a saved plan.
func (m ResourceMode) PersistsPlanToApply() bool {
switch m {
case EphemeralResourceMode:
return false
default:
return true
}
}

// AbsResourceInstanceObject represents one of the specific remote objects
// associated with a resource instance.
//
Expand Down
12 changes: 9 additions & 3 deletions internal/addrs/resourcemode_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading