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) {
|
||||
// This test reaches out to registry.opentofu.org to download the
|
||||
// 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.
|
||||
&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
|
||||
&ProviderFunctionTransformer{Config: b.Config, ProviderFunctionTracker: b.ProviderFunctionTracker},
|
||||
|
||||
|
||||
@@ -93,6 +93,11 @@ func (b *EvalGraphBuilder) Steps() []GraphTransformer {
|
||||
// analyze the configuration to find references.
|
||||
&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
|
||||
&ProviderFunctionTransformer{Config: b.Config, ProviderFunctionTracker: b.ProviderFunctionTracker},
|
||||
|
||||
|
||||
@@ -217,6 +217,11 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer {
|
||||
// analyze the configuration to find references.
|
||||
&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
|
||||
&ProviderFunctionTransformer{Config: b.Config, ProviderFunctionTracker: b.ProviderFunctionTracker},
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ const traceAttrProviderConfigAddr = "opentofu.provider_config.address"
|
||||
// together uniquely identify the provider instance.
|
||||
const traceAttrProviderInstanceAddr = "opentofu.provider_instance.address"
|
||||
|
||||
// NodeApplyableProvider represents a provider during an apply.
|
||||
// NodeApplyableProvider represents a configured provider.
|
||||
type NodeApplyableProvider struct {
|
||||
*NodeAbstractProvider
|
||||
}
|
||||
|
||||
@@ -286,6 +286,39 @@ func (m ProviderFunctionMapping) Lookup(module addrs.Module, pf addrs.ProviderFu
|
||||
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
|
||||
// within the graph. This will error if there are any provider functions that don't map to known providers.
|
||||
type ProviderFunctionTransformer struct {
|
||||
@@ -374,6 +407,7 @@ func (t *ProviderFunctionTransformer) Transform(_ context.Context, g *Graph) err
|
||||
} else {
|
||||
// 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
|
||||
log.Printf("[TRACE] ProviderFunctionTransformer: creating init-only node for %s", absPc)
|
||||
stubAddr := addrs.AbsProviderConfig{
|
||||
Module: addrs.RootModule,
|
||||
Provider: absPc.Provider,
|
||||
|
||||
@@ -10,9 +10,12 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/configs"
|
||||
"github.com/opentofu/opentofu/internal/dag"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func testProviderTransformerGraph(t *testing.T, cfg *configs.Config) *Graph {
|
||||
@@ -48,8 +51,13 @@ func testTransformProviders(concrete ConcreteProviderNodeFunc, config *configs.C
|
||||
&ProviderTransformer{
|
||||
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
|
||||
// &ProviderFunctionTransformer{Config: config},
|
||||
&ProviderFunctionTransformer{Config: config, ProviderFunctionTracker: ProviderFunctionMapping{}},
|
||||
// Remove unused providers and proxies
|
||||
&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 = `
|
||||
aws_instance.web
|
||||
provider["registry.opentofu.org/hashicorp/aws"]
|
||||
|
||||
Reference in New Issue
Block a user