From f08d610702c13ff2bb374f7684726151f0e35307 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Fri, 13 Sep 2024 09:17:44 -0400 Subject: [PATCH] add ephemeral resources to configs --- internal/configs/config.go | 10 ++ internal/configs/config_test.go | 2 + internal/configs/module.go | 42 +++++++- internal/configs/parser_config.go | 11 +++ internal/configs/resource.go | 158 ++++++++++++++++++++++++++++++ 5 files changed, 219 insertions(+), 4 deletions(-) diff --git a/internal/configs/config.go b/internal/configs/config.go index 6d60471a33..c238f53f5f 100644 --- a/internal/configs/config.go +++ b/internal/configs/config.go @@ -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 { @@ -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. diff --git a/internal/configs/config_test.go b/internal/configs/config_test.go index b8e374d72e..9d81d12cc9 100644 --- a/internal/configs/config_test.go +++ b/internal/configs/config_test.go @@ -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" ) func TestConfigProviderTypes(t *testing.T) { diff --git a/internal/configs/module.go b/internal/configs/module.go index d33edc6bc4..94c158e190 100644 --- a/internal/configs/module.go +++ b/internal/configs/module.go @@ -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 @@ -86,8 +87,9 @@ type File struct { ModuleCalls []*ModuleCall - ManagedResources []*Resource - DataResources []*Resource + ManagedResources []*Resource + DataResources []*Resource + EphemeralResources []*Resource Moved []*Moved Removed []*Removed @@ -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{}, @@ -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 } @@ -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() diff --git a/internal/configs/parser_config.go b/internal/configs/parser_config.go index 468809e061..3203b8151a 100644 --- a/internal/configs/parser_config.go +++ b/internal/configs/parser_config.go @@ -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...) @@ -310,6 +317,10 @@ var configFileSchema = &hcl.BodySchema{ Type: "data", LabelNames: []string{"type", "name"}, }, + { + Type: "ephemeral", + LabelNames: []string{"type", "name"}, + }, { Type: "moved", }, diff --git a/internal/configs/resource.go b/internal/configs/resource.go index 40026b9d91..0587b1792b 100644 --- a/internal/configs/resource.go +++ b/internal/configs/resource.go @@ -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) + } + 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{ @@ -783,6 +932,15 @@ var dataBlockSchema = &hcl.BodySchema{ }, } +var ephemeralBlockSchema = &hcl.BodySchema{ + Attributes: commonResourceAttributes, + Blocks: []hcl.BlockHeaderSchema{ + {Type: "lifecycle"}, + {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