mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 17:59:05 -05:00
feat: allow calling functions from unconfigured providers when no resources are referenced (#3118)
Signed-off-by: Diogenes Fernandes <diofeher@gmail.com> Signed-off-by: Diógenes Fernandes <diofeher@gmail.com> Co-authored-by: Andrei Ciobanu <andrei.ciobanu@opentofu.org>
This commit is contained in:
committed by
GitHub
parent
8dc7aa2e24
commit
dfc1a4e948
@@ -53,6 +53,47 @@ func TestFunction_Simple(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFunction_ProviderDefinedFunctionWithoutConfigure(t *testing.T) {
|
||||||
|
// This test reaches out to registry.opentofu.org to download the
|
||||||
|
// test functions provider, so it can only run if network access is allowed
|
||||||
|
skipIfCannotAccessNetwork(t)
|
||||||
|
|
||||||
|
fixturePath := filepath.Join("testdata", "functions_aws")
|
||||||
|
tf := e2e.NewBinary(t, tofuBin, fixturePath)
|
||||||
|
|
||||||
|
// tofu init
|
||||||
|
_, stderr, err := tf.Run("init")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
if stderr != "" {
|
||||||
|
t.Errorf("unexpected stderr output:\n%s", stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, stderr, err = tf.Run("plan", "-out=fnplan")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
if stderr != "" {
|
||||||
|
t.Fatalf("unexpected stderr output:\n%s", stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
plan, err := tf.Plan("fnplan")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(plan.Changes.Outputs) != 1 {
|
||||||
|
t.Fatalf("expected 1 outputs, got %d", len(plan.Changes.Outputs))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, out := range plan.Changes.Outputs {
|
||||||
|
if !strings.Contains(string(out.After), "arn:aws:s3:::bucket-prod") {
|
||||||
|
t.Fatalf("unexpected plan output: %s", string(out.After))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFunction_Error(t *testing.T) {
|
func TestFunction_Error(t *testing.T) {
|
||||||
// This test reaches out to registry.opentofu.org to download the
|
// This test reaches out to registry.opentofu.org to download the
|
||||||
// test functions provider, so it can only run if network access is allowed
|
// test functions provider, so it can only run if network access is allowed
|
||||||
|
|||||||
17
internal/command/e2etest/testdata/functions_aws/main.tf
vendored
Normal file
17
internal/command/e2etest/testdata/functions_aws/main.tf
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
aws = {
|
||||||
|
source = "hashicorp/aws"
|
||||||
|
version = "6.9.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "bucket_name" {
|
||||||
|
type = string
|
||||||
|
default = "bucket-prod"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "stuff" {
|
||||||
|
value = provider::aws::arn_build("aws", "s3", "", "", var.bucket_name)
|
||||||
|
}
|
||||||
@@ -156,6 +156,11 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer {
|
|||||||
// analyze the configuration to find references.
|
// analyze the configuration to find references.
|
||||||
&AttachSchemaTransformer{Plugins: b.Plugins, Config: b.Config},
|
&AttachSchemaTransformer{Plugins: b.Plugins, Config: b.Config},
|
||||||
|
|
||||||
|
// Replace providers that have no config or dependencies to
|
||||||
|
// NodeEvalableProvider. This allows using provider-defined functions
|
||||||
|
// even when the provider isn't configured.
|
||||||
|
&ProviderUnconfiguredTransformer{},
|
||||||
|
|
||||||
// After schema transformer, we can add function references
|
// After schema transformer, we can add function references
|
||||||
&ProviderFunctionTransformer{Config: b.Config, ProviderFunctionTracker: b.ProviderFunctionTracker},
|
&ProviderFunctionTransformer{Config: b.Config, ProviderFunctionTracker: b.ProviderFunctionTracker},
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,11 @@ func (b *EvalGraphBuilder) Steps() []GraphTransformer {
|
|||||||
// analyze the configuration to find references.
|
// analyze the configuration to find references.
|
||||||
&AttachSchemaTransformer{Plugins: b.Plugins, Config: b.Config},
|
&AttachSchemaTransformer{Plugins: b.Plugins, Config: b.Config},
|
||||||
|
|
||||||
|
// Replace providers that have no config or dependencies to
|
||||||
|
// NodeEvalableProvider. This allows using provider-defined functions
|
||||||
|
// even when the provider isn't configured.
|
||||||
|
&ProviderUnconfiguredTransformer{},
|
||||||
|
|
||||||
// After schema transformer, we can add function references
|
// After schema transformer, we can add function references
|
||||||
&ProviderFunctionTransformer{Config: b.Config, ProviderFunctionTracker: b.ProviderFunctionTracker},
|
&ProviderFunctionTransformer{Config: b.Config, ProviderFunctionTracker: b.ProviderFunctionTracker},
|
||||||
|
|
||||||
|
|||||||
@@ -217,6 +217,11 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer {
|
|||||||
// analyze the configuration to find references.
|
// analyze the configuration to find references.
|
||||||
&AttachSchemaTransformer{Plugins: b.Plugins, Config: b.Config},
|
&AttachSchemaTransformer{Plugins: b.Plugins, Config: b.Config},
|
||||||
|
|
||||||
|
// Replace providers that have no config or dependencies to
|
||||||
|
// NodeEvalableProvider. This allows using provider-defined functions
|
||||||
|
// even when the provider isn't configured.
|
||||||
|
&ProviderUnconfiguredTransformer{},
|
||||||
|
|
||||||
// After schema transformer, we can add function references
|
// After schema transformer, we can add function references
|
||||||
&ProviderFunctionTransformer{Config: b.Config, ProviderFunctionTracker: b.ProviderFunctionTracker},
|
&ProviderFunctionTransformer{Config: b.Config, ProviderFunctionTracker: b.ProviderFunctionTracker},
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const traceAttrProviderConfigAddr = "opentofu.provider_config.address"
|
|||||||
// together uniquely identify the provider instance.
|
// together uniquely identify the provider instance.
|
||||||
const traceAttrProviderInstanceAddr = "opentofu.provider_instance.address"
|
const traceAttrProviderInstanceAddr = "opentofu.provider_instance.address"
|
||||||
|
|
||||||
// NodeApplyableProvider represents a provider during an apply.
|
// NodeApplyableProvider represents a configured provider.
|
||||||
type NodeApplyableProvider struct {
|
type NodeApplyableProvider struct {
|
||||||
*NodeAbstractProvider
|
*NodeAbstractProvider
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -286,6 +286,39 @@ func (m ProviderFunctionMapping) Lookup(module addrs.Module, pf addrs.ProviderFu
|
|||||||
return providedBy, ok
|
return providedBy, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProviderUnconfiguredTransformer converts NodeApplyableProvider nodes to NodeEvalableProvider
|
||||||
|
// nodes so provider's functions can be used without configuration.
|
||||||
|
type ProviderUnconfiguredTransformer struct{}
|
||||||
|
|
||||||
|
func (t *ProviderUnconfiguredTransformer) Transform(_ context.Context, g *Graph) error {
|
||||||
|
// Locate all providerVerts in the graph
|
||||||
|
providerVerts := providerVertexMap(g)
|
||||||
|
// Iterate through the providers to identify their dependencies (edges). If a provider
|
||||||
|
// lacks both references and configuration, use a NodeEvalableProvider.
|
||||||
|
for _, p := range providerVerts {
|
||||||
|
applyableProvider, ok := p.(*NodeApplyableProvider)
|
||||||
|
// There are three conditions to skip the conversion
|
||||||
|
// from NodeApplyableProvider to NodeEvalableProvider:
|
||||||
|
// 1. The node is not an NodeApplyableProvider
|
||||||
|
// 2. The provider has existing configuration
|
||||||
|
// 3. The provider node is referenced by another node
|
||||||
|
edges := append(g.EdgesFrom(applyableProvider), g.EdgesTo(applyableProvider)...)
|
||||||
|
if !ok || applyableProvider.Config != nil || len(edges) > 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pAddr := applyableProvider.ProviderAddr()
|
||||||
|
log.Printf("[TRACE] ProviderFunctionTransformer: replacing NodeApplyableProvider with NodeEvalableProvider for %s since it's missing configuration and there are no consumers of it", pAddr)
|
||||||
|
unconfiguredProvider := &NodeEvalableProvider{
|
||||||
|
&NodeAbstractProvider{
|
||||||
|
Addr: pAddr,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
g.Replace(applyableProvider, unconfiguredProvider)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ProviderFunctionTransformer is a GraphTransformer that maps nodes which reference functions to providers
|
// ProviderFunctionTransformer is a GraphTransformer that maps nodes which reference functions to providers
|
||||||
// within the graph. This will error if there are any provider functions that don't map to known providers.
|
// within the graph. This will error if there are any provider functions that don't map to known providers.
|
||||||
type ProviderFunctionTransformer struct {
|
type ProviderFunctionTransformer struct {
|
||||||
@@ -374,6 +407,7 @@ func (t *ProviderFunctionTransformer) Transform(_ context.Context, g *Graph) err
|
|||||||
} else {
|
} else {
|
||||||
// If this provider doesn't exist, stub it out with an init-only provider node
|
// If this provider doesn't exist, stub it out with an init-only provider node
|
||||||
// This works for unconfigured functions only, but that validation is elsewhere
|
// This works for unconfigured functions only, but that validation is elsewhere
|
||||||
|
log.Printf("[TRACE] ProviderFunctionTransformer: creating init-only node for %s", absPc)
|
||||||
stubAddr := addrs.AbsProviderConfig{
|
stubAddr := addrs.AbsProviderConfig{
|
||||||
Module: addrs.RootModule,
|
Module: addrs.RootModule,
|
||||||
Provider: absPc.Provider,
|
Provider: absPc.Provider,
|
||||||
|
|||||||
@@ -10,9 +10,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||||
"github.com/opentofu/opentofu/internal/addrs"
|
"github.com/opentofu/opentofu/internal/addrs"
|
||||||
"github.com/opentofu/opentofu/internal/configs"
|
"github.com/opentofu/opentofu/internal/configs"
|
||||||
"github.com/opentofu/opentofu/internal/dag"
|
"github.com/opentofu/opentofu/internal/dag"
|
||||||
|
"github.com/zclconf/go-cty/cty"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testProviderTransformerGraph(t *testing.T, cfg *configs.Config) *Graph {
|
func testProviderTransformerGraph(t *testing.T, cfg *configs.Config) *Graph {
|
||||||
@@ -48,8 +51,13 @@ func testTransformProviders(concrete ConcreteProviderNodeFunc, config *configs.C
|
|||||||
&ProviderTransformer{
|
&ProviderTransformer{
|
||||||
Config: config,
|
Config: config,
|
||||||
},
|
},
|
||||||
|
// Replace providers that have no config or dependencies to
|
||||||
|
// NodeEvalableProvider. This allows using provider-defined functions
|
||||||
|
// even when the provider isn't configured.
|
||||||
|
&ProviderUnconfiguredTransformer{},
|
||||||
|
|
||||||
// After schema transformer, we can add function references
|
// After schema transformer, we can add function references
|
||||||
// &ProviderFunctionTransformer{Config: config},
|
&ProviderFunctionTransformer{Config: config, ProviderFunctionTracker: ProviderFunctionMapping{}},
|
||||||
// Remove unused providers and proxies
|
// Remove unused providers and proxies
|
||||||
&PruneProviderTransformer{},
|
&PruneProviderTransformer{},
|
||||||
)
|
)
|
||||||
@@ -489,6 +497,78 @@ provider "test" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestProviderFunctionTransformer_onlyFunctions tests that the
|
||||||
|
// ProviderFunctionTransformer is removing NodeApplyableProvider
|
||||||
|
// and adding a NodeEvalableProvider in its place instead.
|
||||||
|
// This is useful so we can call functions without needing to
|
||||||
|
// configure the provider.
|
||||||
|
func TestProviderFunctionTransformer_onlyFunctions(t *testing.T) {
|
||||||
|
mod := testModuleInline(t, map[string]string{
|
||||||
|
"main.tf": `
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
aws = {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "output_test" {
|
||||||
|
value = provider::aws::arn_build("aws", "s3", "", "", "test")
|
||||||
|
}
|
||||||
|
`})
|
||||||
|
concrete := func(a *NodeAbstractProvider) dag.Vertex {
|
||||||
|
return &NodeApplyableProvider{
|
||||||
|
a,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g := testProviderTransformerGraph(t, mod)
|
||||||
|
|
||||||
|
// Create a reference to the output
|
||||||
|
outputRef := &NodeApplyableOutput{
|
||||||
|
Addr: addrs.AbsOutputValue{
|
||||||
|
Module: addrs.RootModuleInstance,
|
||||||
|
OutputValue: addrs.OutputValue{
|
||||||
|
Name: "output_test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Config: &configs.Output{
|
||||||
|
Name: "output_test",
|
||||||
|
Expr: &hclsyntax.FunctionCallExpr{
|
||||||
|
Name: "provider::aws::arn_build",
|
||||||
|
Args: []hclsyntax.Expression{
|
||||||
|
&hclsyntax.LiteralValueExpr{
|
||||||
|
Val: cty.StringVal("aws"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Add(outputRef)
|
||||||
|
|
||||||
|
tf := testTransformProviders(concrete, mod)
|
||||||
|
if err := tf.Transform(t.Context(), g); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := `output.output_test
|
||||||
|
provider["registry.opentofu.org/hashicorp/aws"]
|
||||||
|
provider["registry.opentofu.org/hashicorp/aws"]`
|
||||||
|
|
||||||
|
actual := strings.TrimSpace(g.String())
|
||||||
|
if diff := cmp.Diff(actual, expected); diff != "" {
|
||||||
|
t.Fatalf("expected: %s", diff)
|
||||||
|
}
|
||||||
|
edges := g.EdgesFrom(outputRef)
|
||||||
|
if len(edges) != 1 {
|
||||||
|
t.Fatalf("expecting 1 edge, got %d", len(edges))
|
||||||
|
}
|
||||||
|
edge := edges[0]
|
||||||
|
if _, ok := edge.Target().(*NodeEvalableProvider); !ok {
|
||||||
|
t.Fatalf("expecting NodeEvalableProvider provider, got %T", edge.Target())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const testTransformProviderBasicStr = `
|
const testTransformProviderBasicStr = `
|
||||||
aws_instance.web
|
aws_instance.web
|
||||||
provider["registry.opentofu.org/hashicorp/aws"]
|
provider["registry.opentofu.org/hashicorp/aws"]
|
||||||
|
|||||||
Reference in New Issue
Block a user