Skip to content

Commit

Permalink
(TFECO-7726) Ephemeral Resources support (#411)
Browse files Browse the repository at this point in the history
* support EphemeralResources in provider schema
* bump terraform-json
* early decode ephemeral resources and support legacy inferred provider behaviour for them
* add static schema for ephemeral resources
* merge ephemeral resources from provider schemas
* fix: ephemeral block does not exist in schemas prior to TF 1.10
* fix: prefix key for ephemeral resources
  • Loading branch information
ansgarm authored Oct 23, 2024
1 parent a6f0bbf commit db3b8a6
Show file tree
Hide file tree
Showing 16 changed files with 337 additions and 29 deletions.
26 changes: 26 additions & 0 deletions earlydecoder/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,32 @@ func LoadModule(path string, files map[string]*hcl.File) (*module.Meta, hcl.Diag
}
}

for _, ephemeralResource := range mod.EphemeralResources {
providerName := ephemeralResource.Provider.LocalName

_, err := tfaddr.ParseProviderPart(providerName)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid provider name",
Detail: fmt.Sprintf("%q is not a valid implied provider name: %s", providerName, err),
})
continue
}

localRef := module.ProviderRef{
LocalName: providerName,
}
if _, exists := refs[localRef]; !exists && providerName != "" {
src := addr.NewLegacyProvider(providerName)
if _, exists := providerRequirements[src]; !exists {
providerRequirements[src] = version.Constraints{}
}

refs[localRef] = src
}
}

for _, dataSource := range mod.DataSources {
providerName := dataSource.Provider.LocalName

Expand Down
6 changes: 6 additions & 0 deletions earlydecoder/decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ resource "google_storage_bucket" "bucket" {
name = "test-bucket"
}
ephemeral "random_password" "psst" {
length = 16
}
data "blah_foobar" "test" {
name = "something"
}
Expand All @@ -95,12 +99,14 @@ provider "grafana" {
{LocalName: "blah"}: addr.NewLegacyProvider("blah"),
{LocalName: "google"}: addr.NewLegacyProvider("google"),
{LocalName: "grafana"}: addr.NewLegacyProvider("grafana"),
{LocalName: "random"}: addr.NewLegacyProvider("random"),
},
ProviderRequirements: map[tfaddr.Provider]version.Constraints{
addr.NewLegacyProvider("aws"): {},
addr.NewLegacyProvider("blah"): {},
addr.NewLegacyProvider("google"): {},
addr.NewLegacyProvider("grafana"): {},
addr.NewLegacyProvider("random"): {},
},
Variables: map[string]module.Variable{},
Outputs: map[string]module.Output{},
Expand Down
22 changes: 22 additions & 0 deletions earlydecoder/ephemeral_resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package earlydecoder

import (
"fmt"

"github.com/hashicorp/terraform-schema/module"
)

type ephemeralResource struct {
Type string
Name string
Provider module.ProviderRef
}

// MapKey returns a string that can be used to uniquely identify the receiver
// in a map[string]*ephemeralResource.
func (r *ephemeralResource) MapKey() string {
return fmt.Sprintf("ephemeral.%s.%s", r.Type, r.Name)
}
25 changes: 25 additions & 0 deletions earlydecoder/load_module.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type decodedModule struct {
ProviderRequirements map[string]*providerRequirement
ProviderConfigs map[string]*providerConfig
Resources map[string]*resource
EphemeralResources map[string]*ephemeralResource
DataSources map[string]*dataSource
Variables map[string]*module.Variable
Outputs map[string]*module.Output
Expand All @@ -39,6 +40,7 @@ func newDecodedModule() *decodedModule {
ProviderRequirements: make(map[string]*providerRequirement),
ProviderConfigs: make(map[string]*providerConfig),
Resources: make(map[string]*resource),
EphemeralResources: make(map[string]*ephemeralResource),
DataSources: make(map[string]*dataSource),
Variables: make(map[string]*module.Variable),
Outputs: make(map[string]*module.Output),
Expand Down Expand Up @@ -206,6 +208,29 @@ func loadModuleFromFile(file *hcl.File, mod *decodedModule) hcl.Diagnostics {
}
}

case "ephemeral":
content, _, contentDiags := block.Body.PartialContent(resourceSchema)
diags = append(diags, contentDiags...)

er := &ephemeralResource{
Type: block.Labels[0],
Name: block.Labels[1],
}

mod.EphemeralResources[er.MapKey()] = er

if attr, defined := content.Attributes["provider"]; defined {
ref, aDiags := decodeProviderAttribute(attr)
diags = append(diags, aDiags...)
er.Provider = ref
} else {
// If provider _isn't_ set then we'll infer it from the
// ephemeral resource type.
er.Provider = module.ProviderRef{
LocalName: inferProviderNameFromType(er.Type),
}
}

case "variable":
content, _, contentDiags := block.Body.PartialContent(variableSchema)
diags = append(diags, contentDiags...)
Expand Down
4 changes: 4 additions & 0 deletions earlydecoder/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ var rootSchema = &hcl.BodySchema{
Type: "resource",
LabelNames: []string{"type", "name"},
},
{
Type: "ephemeral",
LabelNames: []string{"type", "name"},
},
{
Type: "data",
LabelNames: []string{"type", "name"},
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ require (
github.com/hashicorp/hcl-lang v0.0.0-20240830144831-468c47ee72a9
github.com/hashicorp/hcl/v2 v2.22.0
github.com/hashicorp/terraform-exec v0.21.0
github.com/hashicorp/terraform-json v0.22.1
github.com/hashicorp/terraform-json v0.22.2-0.20241007092238-76bdbbf21572
github.com/hashicorp/terraform-registry-address v0.2.3
github.com/mh-cbon/go-fmt-fail v0.0.0-20160815164508-67765b3fbcb5
github.com/zclconf/go-cty v1.15.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ github.com/hashicorp/terraform-exec v0.21.0 h1:uNkLAe95ey5Uux6KJdua6+cv8asgILFVW
github.com/hashicorp/terraform-exec v0.21.0/go.mod h1:1PPeMYou+KDUSSeRE9szMZ/oHf4fYUmB923Wzbq1ICg=
github.com/hashicorp/terraform-json v0.22.1 h1:xft84GZR0QzjPVWs4lRUwvTcPnegqlyS7orfb5Ltvec=
github.com/hashicorp/terraform-json v0.22.1/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A=
github.com/hashicorp/terraform-json v0.22.2-0.20241007092238-76bdbbf21572 h1:B7p7ZRTgmNNFZ6jQVz+FZ+/zf56047N5f6gmYKCRJOk=
github.com/hashicorp/terraform-json v0.22.2-0.20241007092238-76bdbbf21572/go.mod h1:MHdXbBAbSg0GvzuWazEGKAn/cyNfIB7mN6y7KJN6y2c=
github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI=
github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM=
github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ=
Expand Down
126 changes: 126 additions & 0 deletions internal/schema/1.10/ephemeral_block.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package schema

import (
"github.com/hashicorp/hcl-lang/lang"
"github.com/hashicorp/hcl-lang/schema"
"github.com/hashicorp/terraform-schema/internal/schema/refscope"
"github.com/hashicorp/terraform-schema/internal/schema/tokmod"
"github.com/zclconf/go-cty/cty"
)

func ephemeralBlockSchema() *schema.BlockSchema {
bs := &schema.BlockSchema{
Address: &schema.BlockAddrSchema{
Steps: []schema.AddrStep{
schema.StaticStep{Name: "ephemeral"},
schema.LabelStep{Index: 0},
schema.LabelStep{Index: 1},
},
FriendlyName: "ephemeral",
ScopeId: refscope.EphemeralScope,
AsReference: true,
DependentBodyAsData: true,
InferDependentBody: true,
DependentBodySelfRef: true,
},
SemanticTokenModifiers: lang.SemanticTokenModifiers{tokmod.Ephemeral},
Labels: []*schema.LabelSchema{
{
Name: "type",
SemanticTokenModifiers: lang.SemanticTokenModifiers{tokmod.Type, lang.TokenModifierDependent},
Description: lang.PlainText("Ephemeral Resource Type"),
IsDepKey: true,
Completable: true,
},
{
Name: "name",
SemanticTokenModifiers: lang.SemanticTokenModifiers{tokmod.Name},
Description: lang.PlainText("Reference Name"),
},
},
Description: lang.PlainText("An ephemeral block declares an ephemeral resource of a given type with a given local name. The name is " +
"used to refer to this ephemeral resource from elsewhere in the same Terraform module, but has no significance " +
"outside of the scope of a module."),
Body: &schema.BodySchema{
Extensions: &schema.BodyExtensions{
Count: true,
ForEach: true,
DynamicBlocks: true,
},
Attributes: map[string]*schema.AttributeSchema{
"provider": {
Constraint: schema.Reference{OfScopeId: refscope.ProviderScope},
IsOptional: true,
Description: lang.Markdown("Reference to a `provider` configuration block, e.g. `mycloud.west` or `mycloud`"),
IsDepKey: true,
SemanticTokenModifiers: lang.SemanticTokenModifiers{lang.TokenModifierDependent},
},
"depends_on": {
Constraint: schema.Set{
Elem: schema.OneOf{
schema.Reference{OfScopeId: refscope.DataScope},
schema.Reference{OfScopeId: refscope.ModuleScope},
schema.Reference{OfScopeId: refscope.ResourceScope},
schema.Reference{OfScopeId: refscope.EphemeralScope},
schema.Reference{OfScopeId: refscope.VariableScope},
schema.Reference{OfScopeId: refscope.LocalScope},
},
},
IsOptional: true,
Description: lang.Markdown("Set of references to hidden dependencies, e.g. resources or data sources"),
},
},
Blocks: map[string]*schema.BlockSchema{
"lifecycle": ephemeralLifecycleBlock(),
},
},
}

return bs
}

func ephemeralLifecycleBlock() *schema.BlockSchema {
return &schema.BlockSchema{
Description: lang.Markdown("Lifecycle customizations to change default ephemeral resource behaviors during apply"),
Body: &schema.BodySchema{
Blocks: map[string]*schema.BlockSchema{
"precondition": {
Body: conditionBody(false),
},
"postcondition": {
Body: conditionBody(true),
},
},
},
}
}

func conditionBody(enableSelfRefs bool) *schema.BodySchema {
bs := &schema.BodySchema{
Attributes: map[string]*schema.AttributeSchema{
"condition": {
Constraint: schema.AnyExpression{OfType: cty.Bool},
IsRequired: true,
Description: lang.Markdown("Condition, a boolean expression that should return `true` " +
"if the intended assumption or guarantee is fulfilled or `false` if it is not."),
},
"error_message": {
Constraint: schema.AnyExpression{OfType: cty.String},
IsRequired: true,
Description: lang.Markdown("Error message to return if the `condition` isn't met " +
"(evaluates to `false`)."),
},
},
}

if enableSelfRefs {
bs.Extensions = &schema.BodyExtensions{
SelfRefs: true,
}
}

return bs
}
20 changes: 20 additions & 0 deletions internal/schema/1.10/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/zclconf/go-cty/cty"

v1_9_mod "github.com/hashicorp/terraform-schema/internal/schema/1.9"
"github.com/hashicorp/terraform-schema/internal/schema/refscope"
)

func ModuleSchema(v *version.Version) *schema.BodySchema {
Expand All @@ -26,5 +27,24 @@ func ModuleSchema(v *version.Version) *schema.BodySchema {
Description: lang.PlainText("Whether the value is ephemeral and should not be persisted in the state"),
}

bs.Blocks["ephemeral"] = ephemeralBlockSchema()

// all the depends_on attributes can refer to ephemeral blocks
constraint := schema.Set{
Elem: schema.OneOf{
schema.Reference{OfScopeId: refscope.DataScope},
schema.Reference{OfScopeId: refscope.ModuleScope},
schema.Reference{OfScopeId: refscope.ResourceScope},
schema.Reference{OfScopeId: refscope.EphemeralScope}, // This one is new, but overriding is easier than adding to each list
schema.Reference{OfScopeId: refscope.VariableScope},
schema.Reference{OfScopeId: refscope.LocalScope},
},
}
bs.Blocks["resource"].Body.Attributes["depends_on"].Constraint = constraint
bs.Blocks["data"].Body.Attributes["depends_on"].Constraint = constraint
bs.Blocks["output"].Body.Attributes["depends_on"].Constraint = constraint
bs.Blocks["module"].Body.Attributes["depends_on"].Constraint = constraint
bs.Blocks["check"].Body.Blocks["data"].Body.Attributes["depends_on"].Constraint = constraint

return bs
}
17 changes: 9 additions & 8 deletions internal/schema/refscope/scopes.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import (
)

var (
BuiltinScope = lang.ScopeId("builtin")
DataScope = lang.ScopeId("data")
LocalScope = lang.ScopeId("local")
ModuleScope = lang.ScopeId("module")
OutputScope = lang.ScopeId("output")
ProviderScope = lang.ScopeId("provider")
ResourceScope = lang.ScopeId("resource")
VariableScope = lang.ScopeId("variable")
BuiltinScope = lang.ScopeId("builtin")
DataScope = lang.ScopeId("data")
LocalScope = lang.ScopeId("local")
ModuleScope = lang.ScopeId("module")
OutputScope = lang.ScopeId("output")
ProviderScope = lang.ScopeId("provider")
ResourceScope = lang.ScopeId("resource")
EphemeralScope = lang.ScopeId("ephemeral")
VariableScope = lang.ScopeId("variable")

ComponentScope = lang.ScopeId("component")
IdentityTokenScope = lang.ScopeId("identity_token")
Expand Down
1 change: 1 addition & 0 deletions internal/schema/tokmod/token_modifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ var (
Output = lang.SemanticTokenModifier("terraform-output")
Provider = lang.SemanticTokenModifier("terraform-provider")
Resource = lang.SemanticTokenModifier("terraform-resource")
Ephemeral = lang.SemanticTokenModifier("terraform-ephemeral")
Provisioner = lang.SemanticTokenModifier("terraform-provisioner")
Connection = lang.SemanticTokenModifier("terraform-connection")
Variable = lang.SemanticTokenModifier("terraform-variable")
Expand Down
12 changes: 9 additions & 3 deletions schema/convert_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ import (

func ProviderSchemaFromJson(jsonSchema *tfjson.ProviderSchema, pAddr tfaddr.Provider) *ProviderSchema {
ps := &ProviderSchema{
Resources: map[string]*schema.BodySchema{},
DataSources: map[string]*schema.BodySchema{},
Functions: map[string]*schema.FunctionSignature{},
Resources: map[string]*schema.BodySchema{},
EphemeralResources: map[string]*schema.BodySchema{},
DataSources: map[string]*schema.BodySchema{},
Functions: map[string]*schema.FunctionSignature{},
}

if jsonSchema.ConfigSchema != nil {
Expand All @@ -34,6 +35,11 @@ func ProviderSchemaFromJson(jsonSchema *tfjson.ProviderSchema, pAddr tfaddr.Prov
ps.Resources[rName].Detail = detailForSrcAddr(pAddr, nil)
}

for erName, erSchema := range jsonSchema.EphemeralResourceSchemas {
ps.EphemeralResources[erName] = bodySchemaFromJson(erSchema.Block)
ps.EphemeralResources[erName].Detail = detailForSrcAddr(pAddr, nil)
}

for dsName, dsSchema := range jsonSchema.DataSourceSchemas {
ps.DataSources[dsName] = bodySchemaFromJson(dsSchema.Block)
ps.DataSources[dsName].Detail = detailForSrcAddr(pAddr, nil)
Expand Down
Loading

0 comments on commit db3b8a6

Please sign in to comment.