Skip to content

Commit

Permalink
configs: Experimental support for ephemeral resources
Browse files Browse the repository at this point in the history
Ephemeral resources, declared using "ephemeral" blocks, represent objects
that are instantiated only for the duration of a single Terraform phase,
and are intended for uses such as temporary network tunnels or
time-limited leases of sensitive values from stores such as HashiCorp
Vault.
  • Loading branch information
apparentlymart committed Apr 29, 2024
1 parent 60365f6 commit e1bf50a
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 4 deletions.
8 changes: 8 additions & 0 deletions internal/configs/config.go
Expand Up @@ -470,6 +470,14 @@ 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,
Expand Down
8 changes: 8 additions & 0 deletions internal/configs/experiments.go
Expand Up @@ -242,6 +242,14 @@ func checkModuleExperiments(m *Module) hcl.Diagnostics {
})
}
}
for _, rc := range m.EphemeralResources {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Ephemeral resources are experimental",
Detail: "This feature is currently an opt-in experiment, subject to change in future releases based on feedback.\n\nActivate the feature for this module by adding ephemeral_values to the list of active experiments.",
Subject: rc.DeclRange.Ptr(),
})
}
}

return diags
Expand Down
40 changes: 36 additions & 4 deletions internal/configs/module.go
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 @@ -125,6 +127,7 @@ func NewModule(primaryFiles, overrideFiles []*File) (*Module, hcl.Diagnostics) {
ModuleCalls: map[string]*ModuleCall{},
ManagedResources: map[string]*Resource{},
DataResources: map[string]*Resource{},
EphemeralResources: map[string]*Resource{},
Checks: map[string]*Check{},
ProviderMetas: map[addrs.Provider]*ProviderMeta{},
Tests: map[string]*TestFile{},
Expand Down Expand Up @@ -372,6 +375,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
Expand Up @@ -193,6 +193,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 @@ -308,6 +315,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
Expand Up @@ -534,6 +534,155 @@ func decodeDataBlock(block *hcl.Block, override, nested bool) (*Resource, hcl.Di
return r, diags
}

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

content, remain, moreDiags := block.Body.PartialContent(dataBlockSchema)
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
}

// decodeReplaceTriggeredBy decodes and does basic validation of the
// replace_triggered_by expressions, ensuring they only contains references to
// a single resource, and the only extra variables are count.index or each.key.
Expand Down Expand Up @@ -783,6 +932,15 @@ var dataBlockSchema = &hcl.BodySchema{
},
}

var ephemeralBlockSchema = &hcl.BodySchema{

Check failure on line 935 in internal/configs/resource.go

View workflow job for this annotation

GitHub Actions / Code Consistency Checks

var ephemeralBlockSchema is unused (U1000)

Check failure on line 935 in internal/configs/resource.go

View workflow job for this annotation

GitHub Actions / Code Consistency Checks

var ephemeralBlockSchema is unused (U1000)
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
Expand Down

0 comments on commit e1bf50a

Please sign in to comment.