mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 17:59:05 -05:00
390 lines
12 KiB
Go
390 lines
12 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package eval_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"iter"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/zclconf/go-cty-debug/ctydebug"
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/opentofu/opentofu/internal/addrs"
|
|
"github.com/opentofu/opentofu/internal/configs"
|
|
"github.com/opentofu/opentofu/internal/configs/configschema"
|
|
"github.com/opentofu/opentofu/internal/lang/eval"
|
|
"github.com/opentofu/opentofu/internal/lang/eval/internal/evalglue"
|
|
"github.com/opentofu/opentofu/internal/plans/objchange"
|
|
"github.com/opentofu/opentofu/internal/providers"
|
|
"github.com/opentofu/opentofu/internal/tfdiags"
|
|
)
|
|
|
|
// This file is in "package eval_test" in order to integration-test the
|
|
// validation phase through the same exported API that external callers would
|
|
// use.
|
|
|
|
func TestPlan_valuesOnlySuccess(t *testing.T) {
|
|
// This test has an intentionally limited scope covering just the
|
|
// basics, so that we don't necessarily need to repeat these basics
|
|
// across all of the other tests.
|
|
|
|
configInst, diags := eval.NewConfigInstance(t.Context(), &eval.ConfigCall{
|
|
EvalContext: evalglue.EvalContextForTesting(t, &eval.EvalContext{
|
|
Modules: eval.ModulesForTesting(map[addrs.ModuleSourceLocal]*configs.Module{
|
|
addrs.ModuleSourceLocal("."): configs.ModuleFromStringForTesting(t, `
|
|
variable "a" {
|
|
type = string
|
|
}
|
|
locals {
|
|
b = "${var.a}:${var.a}"
|
|
}
|
|
output "c" {
|
|
value = "${local.b}/${local.b}"
|
|
}
|
|
`),
|
|
}),
|
|
}),
|
|
RootModuleSource: addrs.ModuleSourceLocal("."),
|
|
InputValues: eval.InputValuesForTesting(map[string]cty.Value{
|
|
"a": cty.True,
|
|
}),
|
|
})
|
|
if diags.HasErrors() {
|
|
t.Fatalf("unexpected errors: %s", diags.Err())
|
|
}
|
|
|
|
logGlue := &planGlueCallLog{}
|
|
planResult, diags := configInst.DrivePlanning(t.Context(), func(oracle *eval.PlanningOracle) eval.PlanGlue {
|
|
logGlue.oracle = oracle
|
|
return logGlue
|
|
})
|
|
if diags.HasErrors() {
|
|
t.Fatalf("unexpected errors: %s", diags.Err())
|
|
}
|
|
|
|
gotOutputs := planResult.RootModuleOutputs
|
|
wantOutputs := cty.ObjectVal(map[string]cty.Value{
|
|
"c": cty.StringVal("true:true/true:true"),
|
|
})
|
|
if diff := cmp.Diff(wantOutputs, gotOutputs, ctydebug.CmpOptions); diff != "" {
|
|
t.Error("wrong result\n" + diff)
|
|
}
|
|
}
|
|
|
|
func TestPlan_managedResourceSimple(t *testing.T) {
|
|
// This test has an intentionally limited scope covering just the
|
|
// basics, so that we don't necessarily need to repeat these basics
|
|
// across all of the other tests.
|
|
|
|
providers := eval.ProvidersForTesting(map[addrs.Provider]*providers.GetProviderSchemaResponse{
|
|
addrs.MustParseProviderSourceString("test/foo"): {
|
|
Provider: providers.Schema{
|
|
Block: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"greeting": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
ResourceTypes: map[string]providers.Schema{
|
|
"foo": {
|
|
Block: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"name": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
configInst, diags := eval.NewConfigInstance(t.Context(), &eval.ConfigCall{
|
|
EvalContext: evalglue.EvalContextForTesting(t, &eval.EvalContext{
|
|
Modules: eval.ModulesForTesting(map[addrs.ModuleSourceLocal]*configs.Module{
|
|
addrs.ModuleSourceLocal("."): configs.ModuleFromStringForTesting(t, `
|
|
terraform {
|
|
required_providers {
|
|
foo = {
|
|
source = "test/foo"
|
|
}
|
|
}
|
|
}
|
|
provider "foo" {
|
|
greeting = "Hello"
|
|
}
|
|
variable "a" {
|
|
type = string
|
|
}
|
|
resource "foo" "bar" {
|
|
name = var.a
|
|
}
|
|
output "c" {
|
|
value = foo.bar.name
|
|
}
|
|
`),
|
|
}),
|
|
Providers: providers,
|
|
}),
|
|
RootModuleSource: addrs.ModuleSourceLocal("."),
|
|
InputValues: eval.InputValuesForTesting(map[string]cty.Value{
|
|
"a": cty.StringVal("foo bar name"),
|
|
}),
|
|
})
|
|
if diags.HasErrors() {
|
|
t.Fatalf("unexpected errors: %s", diags.Err())
|
|
}
|
|
|
|
logGlue := &planGlueCallLog{
|
|
providers: providers,
|
|
}
|
|
planResult, diags := configInst.DrivePlanning(t.Context(), func(oracle *eval.PlanningOracle) eval.PlanGlue {
|
|
logGlue.oracle = oracle
|
|
return logGlue
|
|
})
|
|
if diags.HasErrors() {
|
|
t.Fatalf("unexpected errors: %s", diags.Err())
|
|
}
|
|
|
|
gotOutputs := planResult.RootModuleOutputs
|
|
wantOutputs := cty.ObjectVal(map[string]cty.Value{
|
|
"c": cty.StringVal("foo bar name"),
|
|
})
|
|
if diff := cmp.Diff(wantOutputs, gotOutputs, ctydebug.CmpOptions); diff != "" {
|
|
t.Error("wrong result\n" + diff)
|
|
}
|
|
|
|
instAddr := addrs.Resource{
|
|
Mode: addrs.ManagedResourceMode,
|
|
Type: "foo",
|
|
Name: "bar",
|
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
|
|
gotReqs := logGlue.resourceInstanceRequests
|
|
wantReqs := addrs.MakeMap(
|
|
addrs.MakeMapElem(instAddr, &eval.DesiredResourceInstance{
|
|
Addr: instAddr,
|
|
ConfigVal: cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.StringVal("foo bar name"),
|
|
}),
|
|
Provider: addrs.MustParseProviderSourceString("test/foo"),
|
|
ProviderInstance: &addrs.AbsProviderInstanceCorrect{
|
|
Config: addrs.AbsProviderConfigCorrect{
|
|
Config: addrs.ProviderConfigCorrect{
|
|
Provider: addrs.MustParseProviderSourceString("test/foo"),
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
)
|
|
if diff := cmp.Diff(wantReqs, gotReqs, ctydebug.CmpOptions); diff != "" {
|
|
t.Error("wrong requests\n" + diff)
|
|
}
|
|
|
|
providerInstAddr := addrs.AbsProviderInstanceCorrect{
|
|
Config: addrs.AbsProviderConfigCorrect{
|
|
Config: addrs.ProviderConfigCorrect{
|
|
Provider: addrs.MustParseProviderSourceString("test/foo"),
|
|
},
|
|
},
|
|
}
|
|
gotProviderInstConfigs := logGlue.providerInstanceConfigs
|
|
wantProviderInstConfigs := addrs.MakeMap(
|
|
addrs.MakeMapElem(providerInstAddr, cty.ObjectVal(map[string]cty.Value{
|
|
"greeting": cty.StringVal("Hello"),
|
|
})),
|
|
)
|
|
if diff := cmp.Diff(wantProviderInstConfigs, gotProviderInstConfigs, ctydebug.CmpOptions); diff != "" {
|
|
t.Error("wrong provider instance configs\n" + diff)
|
|
}
|
|
}
|
|
|
|
func TestPlan_managedResourceUnknownCount(t *testing.T) {
|
|
// This test has an intentionally limited scope covering just the
|
|
// basics, so that we don't necessarily need to repeat these basics
|
|
// across all of the other tests.
|
|
|
|
providers := eval.ProvidersForTesting(map[addrs.Provider]*providers.GetProviderSchemaResponse{
|
|
addrs.MustParseProviderSourceString("test/foo"): {
|
|
ResourceTypes: map[string]providers.Schema{
|
|
"foo": {
|
|
Block: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"name": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
configInst, diags := eval.NewConfigInstance(t.Context(), &eval.ConfigCall{
|
|
EvalContext: evalglue.EvalContextForTesting(t, &eval.EvalContext{
|
|
Modules: eval.ModulesForTesting(map[addrs.ModuleSourceLocal]*configs.Module{
|
|
addrs.ModuleSourceLocal("."): configs.ModuleFromStringForTesting(t, `
|
|
terraform {
|
|
required_providers {
|
|
foo = {
|
|
source = "test/foo"
|
|
}
|
|
}
|
|
}
|
|
variable "a" {
|
|
type = string
|
|
}
|
|
variable "num" {
|
|
type = number
|
|
}
|
|
resource "foo" "bar" {
|
|
count = var.num
|
|
|
|
name = var.a
|
|
}
|
|
output "c" {
|
|
value = foo.bar[*].name
|
|
}
|
|
`),
|
|
}),
|
|
Providers: providers,
|
|
}),
|
|
RootModuleSource: addrs.ModuleSourceLocal("."),
|
|
InputValues: eval.InputValuesForTesting(map[string]cty.Value{
|
|
"a": cty.StringVal("foo bar name"),
|
|
"num": cty.UnknownVal(cty.Number),
|
|
}),
|
|
})
|
|
if diags.HasErrors() {
|
|
t.Fatalf("unexpected errors: %s", diags.Err())
|
|
}
|
|
|
|
logGlue := &planGlueCallLog{
|
|
providers: providers,
|
|
}
|
|
planResult, diags := configInst.DrivePlanning(t.Context(), func(oracle *eval.PlanningOracle) eval.PlanGlue {
|
|
logGlue.oracle = oracle
|
|
return logGlue
|
|
})
|
|
if diags.HasErrors() {
|
|
t.Fatalf("unexpected errors: %s", diags.Err())
|
|
}
|
|
|
|
gotOutputs := planResult.RootModuleOutputs
|
|
wantOutputs := cty.ObjectVal(map[string]cty.Value{
|
|
"c": cty.DynamicVal, // don't know what instances we have yet
|
|
})
|
|
if diff := cmp.Diff(wantOutputs, gotOutputs, ctydebug.CmpOptions); diff != "" {
|
|
t.Error("wrong result\n" + diff)
|
|
}
|
|
|
|
// Because count is unknown, we plan a placeholder resource instance
|
|
// whose instance key is a wildcard.
|
|
instAddr := addrs.Resource{
|
|
Mode: addrs.ManagedResourceMode,
|
|
Type: "foo",
|
|
Name: "bar",
|
|
}.Instance(addrs.WildcardKey{addrs.IntKeyType}).Absolute(addrs.RootModuleInstance)
|
|
gotReqs := logGlue.resourceInstanceRequests
|
|
wantReqs := addrs.MakeMap(
|
|
addrs.MakeMapElem(instAddr, &eval.DesiredResourceInstance{
|
|
Addr: instAddr,
|
|
ConfigVal: cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.StringVal("foo bar name"),
|
|
}),
|
|
Provider: addrs.MustParseProviderSourceString("test/foo"),
|
|
ProviderInstance: &addrs.AbsProviderInstanceCorrect{
|
|
Config: addrs.AbsProviderConfigCorrect{
|
|
Config: addrs.ProviderConfigCorrect{
|
|
Provider: addrs.MustParseProviderSourceString("test/foo"),
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
)
|
|
if diff := cmp.Diff(wantReqs, gotReqs, ctydebug.CmpOptions); diff != "" {
|
|
t.Error("wrong requests\n" + diff)
|
|
}
|
|
}
|
|
|
|
type planGlueCallLog struct {
|
|
oracle *eval.PlanningOracle
|
|
providers eval.ProvidersSchema
|
|
|
|
resourceInstanceRequests addrs.Map[addrs.AbsResourceInstance, *eval.DesiredResourceInstance]
|
|
providerInstanceConfigs addrs.Map[addrs.AbsProviderInstanceCorrect, cty.Value]
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// ValidateProviderConfig implements eval.PlanGlue
|
|
func (p *planGlueCallLog) ValidateProviderConfig(ctx context.Context, provider addrs.Provider, configVal cty.Value) tfdiags.Diagnostics {
|
|
return nil
|
|
}
|
|
|
|
// PlanDesiredResourceInstance implements eval.PlanGlue.
|
|
func (p *planGlueCallLog) PlanDesiredResourceInstance(ctx context.Context, inst *eval.DesiredResourceInstance) (cty.Value, tfdiags.Diagnostics) {
|
|
p.mu.Lock()
|
|
if p.resourceInstanceRequests.Len() == 0 {
|
|
p.resourceInstanceRequests = addrs.MakeMap[addrs.AbsResourceInstance, *eval.DesiredResourceInstance]()
|
|
}
|
|
p.resourceInstanceRequests.Put(inst.Addr, inst)
|
|
if inst.ProviderInstance != nil {
|
|
if p.providerInstanceConfigs.Len() == 0 {
|
|
p.providerInstanceConfigs = addrs.MakeMap[addrs.AbsProviderInstanceCorrect, cty.Value]()
|
|
}
|
|
providerInstAddr := *inst.ProviderInstance
|
|
providerInstConfig := p.oracle.ProviderInstanceConfig(ctx, providerInstAddr)
|
|
p.providerInstanceConfigs.Put(providerInstAddr, providerInstConfig)
|
|
}
|
|
p.mu.Unlock()
|
|
|
|
if p.providers == nil {
|
|
var diags tfdiags.Diagnostics
|
|
diags = diags.Append(errors.New("cannot use resources in this test without including an eval.Providers object to the planGlueCallLog object"))
|
|
return cty.DynamicVal, diags
|
|
}
|
|
schema, diags := p.providers.ResourceTypeSchema(ctx, inst.Provider, inst.Addr.Resource.Resource.Mode, inst.Addr.Resource.Resource.Type)
|
|
if diags.HasErrors() {
|
|
return cty.DynamicVal, diags
|
|
}
|
|
plannedVal := objchange.ProposedNew(schema.Block, cty.NullVal(schema.Block.ImpliedType()), inst.ConfigVal)
|
|
return plannedVal, diags
|
|
}
|
|
|
|
// PlanModuleCallInstanceOrphans implements eval.PlanGlue.
|
|
func (p *planGlueCallLog) PlanModuleCallInstanceOrphans(ctx context.Context, moduleCallAddr addrs.AbsModuleCall, desiredInstances iter.Seq[addrs.InstanceKey]) tfdiags.Diagnostics {
|
|
// We don't currently do anything with calls to this method, because
|
|
// no tests we've written so far rely on it.
|
|
return nil
|
|
}
|
|
|
|
// PlanModuleCallOrphans implements eval.PlanGlue.
|
|
func (p *planGlueCallLog) PlanModuleCallOrphans(ctx context.Context, callerModuleInstAddr addrs.ModuleInstance, desiredCalls iter.Seq[addrs.ModuleCall]) tfdiags.Diagnostics {
|
|
// We don't currently do anything with calls to this method, because
|
|
// no tests we've written so far rely on it.
|
|
return nil
|
|
}
|
|
|
|
// PlanResourceInstanceOrphans implements eval.PlanGlue.
|
|
func (p *planGlueCallLog) PlanResourceInstanceOrphans(ctx context.Context, resourceAddr addrs.AbsResource, desiredInstances iter.Seq[addrs.InstanceKey]) tfdiags.Diagnostics {
|
|
// We don't currently do anything with calls to this method, because
|
|
// no tests we've written so far rely on it.
|
|
return nil
|
|
}
|
|
|
|
// PlanResourceOrphans implements eval.PlanGlue.
|
|
func (p *planGlueCallLog) PlanResourceOrphans(ctx context.Context, moduleInstAddr addrs.ModuleInstance, desiredResources iter.Seq[addrs.Resource]) tfdiags.Diagnostics {
|
|
// We don't currently do anything with calls to this method, because
|
|
// no tests we've written so far rely on it.
|
|
return nil
|
|
}
|