Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions internal/configs/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@ func (c *Config) addProviderRequirements(reqs providerreqs.Requirements, recurse
}
reqs[fqn] = nil
}

for _, rc := range c.Module.DataResources {
fqn := rc.Provider
if _, exists := reqs[fqn]; exists {
Expand All @@ -467,6 +468,15 @@ func (c *Config) addProviderRequirements(reqs providerreqs.Requirements, recurse
reqs[fqn] = nil
}

for _, rc := range c.Module.EphemeralResources {
fqn := rc.Provider
if _, exists := reqs[fqn]; exists {
// Explicit dependency already present
continue
}
reqs[fqn] = nil
}

// Import blocks that are generating config may have a custom provider
// meta-argument. Like the provider meta-argument used in resource blocks,
// we use this opportunity to load any implicit providers.
Expand Down
2 changes: 2 additions & 0 deletions internal/configs/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/depsfile"
"github.com/hashicorp/terraform/internal/getproviders/providerreqs"

_ "github.com/hashicorp/terraform/internal/logging"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this intentional or was it just to temporarily aid debugging in test environment?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have to import the internal logging package somewhere to initialize the default logger, so any package tested in isolation needs this to not print all logs during tests.

)

func TestConfigProviderTypes(t *testing.T) {
Expand Down
42 changes: 38 additions & 4 deletions internal/configs/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ type Module struct {

ModuleCalls map[string]*ModuleCall

ManagedResources map[string]*Resource
DataResources map[string]*Resource
ManagedResources map[string]*Resource
DataResources map[string]*Resource
EphemeralResources map[string]*Resource

Moved []*Moved
Removed []*Removed
Expand Down Expand Up @@ -86,8 +87,9 @@ type File struct {

ModuleCalls []*ModuleCall

ManagedResources []*Resource
DataResources []*Resource
ManagedResources []*Resource
DataResources []*Resource
EphemeralResources []*Resource

Moved []*Moved
Removed []*Removed
Expand Down Expand Up @@ -124,6 +126,7 @@ func NewModule(primaryFiles, overrideFiles []*File) (*Module, hcl.Diagnostics) {
Outputs: map[string]*Output{},
ModuleCalls: map[string]*ModuleCall{},
ManagedResources: map[string]*Resource{},
EphemeralResources: map[string]*Resource{},
DataResources: map[string]*Resource{},
Checks: map[string]*Check{},
ProviderMetas: map[addrs.Provider]*ProviderMeta{},
Expand Down Expand Up @@ -192,6 +195,8 @@ func (m *Module) ResourceByAddr(addr addrs.Resource) *Resource {
return m.ManagedResources[key]
case addrs.DataResourceMode:
return m.DataResources[key]
case addrs.EphemeralResourceMode:
return m.EphemeralResources[key]
default:
return nil
}
Expand Down Expand Up @@ -372,6 +377,35 @@ func (m *Module) appendFile(file *File) hcl.Diagnostics {
m.DataResources[key] = r
}

for _, r := range file.EphemeralResources {
key := r.moduleUniqueKey()
if existing, exists := m.EphemeralResources[key]; exists {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Duplicate ephemeral %q configuration", existing.Type),
Detail: fmt.Sprintf("A %s ephemeral resource named %q was already declared at %s. Resource names must be unique per type in each module.", existing.Type, existing.Name, existing.DeclRange),
Subject: &r.DeclRange,
})
continue
}
m.EphemeralResources[key] = r

// set the provider FQN for the resource
if r.ProviderConfigRef != nil {
r.Provider = m.ProviderForLocalConfig(r.ProviderConfigAddr())
} else {
// an invalid resource name (for e.g. "null resource" instead of
// "null_resource") can cause a panic down the line in addrs:
// https://github.com/hashicorp/terraform/issues/25560
implied, err := addrs.ParseProviderPart(r.Addr().ImpliedProvider())
if err == nil {
r.Provider = m.ImpliedProviderForUnqualifiedType(implied)
}
// We don't return a diagnostic because the invalid resource name
// will already have been caught.
}
}

for _, c := range file.Checks {
if c.DataResource != nil {
key := c.DataResource.moduleUniqueKey()
Expand Down
11 changes: 11 additions & 0 deletions internal/configs/parser_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,13 @@ func parseConfigFile(body hcl.Body, diags hcl.Diagnostics, override, allowExperi
file.DataResources = append(file.DataResources, cfg)
}

case "ephemeral":
cfg, cfgDiags := decodeEphemeralBlock(block, override)
diags = append(diags, cfgDiags...)
if cfg != nil {
file.EphemeralResources = append(file.EphemeralResources, cfg)
}

case "moved":
cfg, cfgDiags := decodeMovedBlock(block)
diags = append(diags, cfgDiags...)
Expand Down Expand Up @@ -310,6 +317,10 @@ var configFileSchema = &hcl.BodySchema{
Type: "data",
LabelNames: []string{"type", "name"},
},
{
Type: "ephemeral",
LabelNames: []string{"type", "name"},
},
{
Type: "moved",
},
Expand Down
158 changes: 158 additions & 0 deletions internal/configs/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,155 @@ func decodeResourceBlock(block *hcl.Block, override bool) (*Resource, hcl.Diagno
return r, diags
}

func decodeEphemeralBlock(block *hcl.Block, override bool) (*Resource, hcl.Diagnostics) {
var diags hcl.Diagnostics
r := &Resource{
Mode: addrs.EphemeralResourceMode,
Type: block.Labels[0],
Name: block.Labels[1],
DeclRange: block.DefRange,
TypeRange: block.LabelRanges[0],
}

content, remain, moreDiags := block.Body.PartialContent(ephemeralBlockSchema)
diags = append(diags, moreDiags...)
r.Config = remain

if !hclsyntax.ValidIdentifier(r.Type) {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid ephemeral resource type",
Detail: badIdentifierDetail,
Subject: &block.LabelRanges[0],
})
}
if !hclsyntax.ValidIdentifier(r.Name) {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid ephemeral resource name",
Detail: badIdentifierDetail,
Subject: &block.LabelRanges[1],
})
}

if attr, exists := content.Attributes["count"]; exists {
r.Count = attr.Expr
}

if attr, exists := content.Attributes["for_each"]; exists {
r.ForEach = attr.Expr
// Cannot have count and for_each on the same ephemeral block
if r.Count != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Invalid combination of "count" and "for_each"`,
Detail: `The "count" and "for_each" meta-arguments are mutually-exclusive, only one should be used to be explicit about the number of resources to be created.`,
Subject: &attr.NameRange,
})
}
}

if attr, exists := content.Attributes["provider"]; exists {
var providerDiags hcl.Diagnostics
r.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr.Expr, "provider")
diags = append(diags, providerDiags...)
}

if attr, exists := content.Attributes["depends_on"]; exists {
deps, depsDiags := DecodeDependsOn(attr)
diags = append(diags, depsDiags...)
r.DependsOn = append(r.DependsOn, deps...)
}

var seenEscapeBlock *hcl.Block
var seenLifecycle *hcl.Block
for _, block := range content.Blocks {
switch block.Type {

case "_":
if seenEscapeBlock != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Duplicate escaping block",
Detail: fmt.Sprintf(
"The special block type \"_\" can be used to force particular arguments to be interpreted as resource-type-specific rather than as meta-arguments, but each data block can have only one such block. The first escaping block was at %s.",
seenEscapeBlock.DefRange,
),
Subject: &block.DefRange,
})
continue
}
seenEscapeBlock = block

// When there's an escaping block its content merges with the
// existing config we extracted earlier, so later decoding
// will see a blend of both.
r.Config = hcl.MergeBodies([]hcl.Body{r.Config, block.Body})

case "lifecycle":
if seenLifecycle != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Duplicate lifecycle block",
Detail: fmt.Sprintf("This resource already has a lifecycle block at %s.", seenLifecycle.DefRange),
Subject: block.DefRange.Ptr(),
})
continue
}
seenLifecycle = block

lcContent, lcDiags := block.Body.Content(resourceLifecycleBlockSchema)
diags = append(diags, lcDiags...)

// All of the attributes defined for resource lifecycle are for
// managed resources only, so we can emit a common error message
// for any given attributes that HCL accepted.
for name, attr := range lcContent.Attributes {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid ephemeral resource lifecycle argument",
Detail: fmt.Sprintf("The lifecycle argument %q is defined only for managed resources (\"resource\" blocks), and is not valid for ephemeral resources.", name),
Subject: attr.NameRange.Ptr(),
})
}

for _, block := range lcContent.Blocks {
switch block.Type {
case "precondition", "postcondition":
cr, moreDiags := decodeCheckRuleBlock(block, override)
diags = append(diags, moreDiags...)

moreDiags = cr.validateSelfReferences(block.Type, r.Addr())
diags = append(diags, moreDiags...)

switch block.Type {
case "precondition":
r.Preconditions = append(r.Preconditions, cr)
case "postcondition":
r.Postconditions = append(r.Postconditions, cr)
}
Comment on lines +475 to +487
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how much complexity/effort will be required to make conditions work for the new resource type but we could also just reserve these in the language initially. You are more likely to know the answer though, so I'm assuming if you are proposing to keep it like this, it's not too complex.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aside from optimistically hoping that the implementation isn't too difficult (these are still just a resource type after all), they also seem useful when the values are not easily extracted by the user for other debugging purposes. If there is any problem implementing later steps we can always reserve the block names to prevent their use. These first commits are all preliminary anyway, there will probably be some changes as all the pieces are integrated.

default:
// The cases above should be exhaustive for all block types
// defined in the lifecycle schema, so this shouldn't happen.
panic(fmt.Sprintf("unexpected lifecycle sub-block type %q", block.Type))
}
}

default:
// Any other block types are ones we're reserving for future use,
// but don't have any defined meaning today.
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Reserved block type name in ephemeral block",
Detail: fmt.Sprintf("The block type name %q is reserved for use by Terraform in a future version.", block.Type),
Subject: block.TypeRange.Ptr(),
})
}
}

return r, diags
}

func decodeDataBlock(block *hcl.Block, override, nested bool) (*Resource, hcl.Diagnostics) {
var diags hcl.Diagnostics
r := &Resource{
Expand Down Expand Up @@ -783,6 +932,15 @@ var dataBlockSchema = &hcl.BodySchema{
},
}

var ephemeralBlockSchema = &hcl.BodySchema{
Attributes: commonResourceAttributes,
Blocks: []hcl.BlockHeaderSchema{
{Type: "lifecycle"},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{Type: "lifecycle"},
{Type: "lifecycle"}, // reserved for future use

{Type: "locals"}, // reserved for future use
{Type: "_"}, // meta-argument escaping block
},
}

var resourceLifecycleBlockSchema = &hcl.BodySchema{
// We tell HCL that these elements are all valid for both "resource"
// and "data" lifecycle blocks, but the rules are actually more restrictive
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ephemeral "test_resource" "test" {
lifecycle {
create_before_destroy = true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ephemeral "test resource" "nope" {
}
9 changes: 9 additions & 0 deletions internal/configs/testdata/valid-files/resources.tf
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,12 @@ resource "aws_instance" "depends" {
replace_triggered_by = [ aws_instance.web[1], aws_security_group.firewall.id ]
}
}

ephemeral "aws_connect" "tunnel" {
}

ephemeral "aws_secret" "auth" {
for_each = local.auths
input = each.value
depends_on = [aws_instance.depends]
}
5 changes: 4 additions & 1 deletion internal/lang/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceabl
// that's redundant in the process of populating our values map.
dataResources := map[string]map[string]cty.Value{}
managedResources := map[string]map[string]cty.Value{}
ephemeralResources := map[string]map[string]cty.Value{}
wholeModules := map[string]cty.Value{}
inputVariables := map[string]cty.Value{}
localValues := map[string]cty.Value{}
Expand Down Expand Up @@ -365,6 +366,8 @@ func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceabl
into = managedResources
case addrs.DataResourceMode:
into = dataResources
case addrs.EphemeralResourceMode:
into = ephemeralResources
default:
panic(fmt.Errorf("unsupported ResourceMode %s", subj.Mode))
}
Expand Down Expand Up @@ -443,7 +446,7 @@ func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceabl
vals[k] = v
}
vals["resource"] = cty.ObjectVal(buildResourceObjects(managedResources))

vals["ephemeral"] = cty.ObjectVal(buildResourceObjects(ephemeralResources))
vals["data"] = cty.ObjectVal(buildResourceObjects(dataResources))
vals["module"] = cty.ObjectVal(wholeModules)
vals["var"] = cty.ObjectVal(inputVariables)
Expand Down
15 changes: 15 additions & 0 deletions internal/lang/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ func TestScopeEvalContext(t *testing.T) {
"data.null_data_source.foo": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("bar"),
}),
"ephemeral.null_secret.foo": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("ephemeral"),
}),
"null_resource.multi": cty.TupleVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("multi0"),
Expand Down Expand Up @@ -320,6 +323,18 @@ func TestScopeEvalContext(t *testing.T) {
}),
},
},
{
Expr: `ephemeral.null_secret.foo`,
Want: map[string]cty.Value{
"ephemeral": cty.ObjectVal(map[string]cty.Value{
"null_secret": cty.ObjectVal(map[string]cty.Value{
"foo": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("ephemeral"),
}),
}),
}),
},
},
{
Expr: `module.foo`,
Want: map[string]cty.Value{
Expand Down
Loading