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 Values prototype #35078

Draft
wants to merge 32 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
7f1ed47
lang: A new mark type for ephemeral values
apparentlymart Feb 29, 2024
0217d4f
experiments: "ephemeral_values" language experiment
apparentlymart Feb 29, 2024
3126ddf
configs: Variables and outputs can be "ephemeral"
apparentlymart Feb 29, 2024
9151909
terraform: Initial partial support for ephemeral values
apparentlymart Feb 29, 2024
9db5857
plans: Track input variables that must be re-supplied during apply
apparentlymart Apr 18, 2024
a2c26e5
terraform: Check for required input variables during the apply phase
apparentlymart Apr 20, 2024
cc0bb71
backend/local: Handle apply-time values for ephemeral input variables
apparentlymart Apr 22, 2024
d4cf5a2
providers: New Interface methods for ephemeral resource types
apparentlymart Apr 29, 2024
e4ec3d3
addrs: EphemeralResourceMode
apparentlymart Apr 29, 2024
2cd0271
configs: Experimental support for ephemeral resources
apparentlymart Apr 29, 2024
faf08fa
terraform provider: terraform_random_number ephemeral resource type
apparentlymart Apr 29, 2024
2504b36
addrs: ParseRef and ParseTarget support ephemeral resource addresses
apparentlymart Apr 30, 2024
e0bed89
terraform: Add ephemeral resources to the graph, and validate refs
apparentlymart Apr 30, 2024
dc14058
lang: Basic awareness of ephemeral resource evaluation
apparentlymart Apr 30, 2024
a1cb700
terraform: Don't panic when visiting ephemeral resource nodes
apparentlymart Apr 30, 2024
ce438c4
terraform: Graph nodes for closing ephemeral resource instances
apparentlymart May 1, 2024
d6f04d1
resources/ephemeral: A place to track ephemeral resource instances
apparentlymart May 2, 2024
8b98ccd
terraform: Plumb in an ephemeral.Resources object for each graph walk
apparentlymart May 3, 2024
05d61a4
terraform: "Close" the graph walker when a graph walker is complete
apparentlymart May 3, 2024
4dedb78
terraform: Open and close ephemeral resource instances during graph walk
apparentlymart May 3, 2024
2e79da5
terraform: Close provider after ephemeral resources closed
apparentlymart May 6, 2024
5d00b8c
terraform: Use GraphNodeReferencer directly for ephemeral resource an…
apparentlymart May 6, 2024
8b48a5b
terraform: Expression evaluator can deal with ephemeral resource refs
apparentlymart May 6, 2024
7f7d2f4
terraform: Never prune "unused" ephemeral resource nodes
apparentlymart May 7, 2024
9dc96e1
plans+states: Reject attempts to persist ephemeral resources
apparentlymart May 7, 2024
65ce265
terraform: Don't try to write ephemeral resources to plan or state
apparentlymart May 7, 2024
cb5006f
command: "terraform apply" accepts variable values with saved plan
apparentlymart May 7, 2024
23ebf2d
terraform: Propagate apply-time variables from plan to apply
apparentlymart May 7, 2024
6884a65
builtin/providers/terraform: Prepare for more ephemeral resource types
apparentlymart May 10, 2024
0a1b9d3
terraform: Better error message for inconsistent ephemeral resource r…
apparentlymart May 10, 2024
de22ac5
builtin/providers/terraform: terraform_ssh_tunnels ephemeral resource…
apparentlymart May 10, 2024
9b8cb54
terraform: Ephemeral resource close comes after provider close
apparentlymart May 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
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
13 changes: 12 additions & 1 deletion internal/addrs/parse_target.go
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
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.