mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
300/provider foreach (#1911)
Signed-off-by: Andrew Hayes <andrew.hayes@harness.io>
This commit is contained in:
parent
7a02fad996
commit
389f33fdc5
@ -11,6 +11,7 @@ ENHANCEMENTS:
|
||||
* Added multi-line support to the `tofu console` command. ([#1307](https://github.com/opentofu/opentofu/issues/1307))
|
||||
* Added a help target to the Makefile. ([#1925](https://github.com/opentofu/opentofu/pull/1925))
|
||||
* Added a simplified Build Process with a Makefile Target ([#1926](https://github.com/opentofu/opentofu/issues/1926))
|
||||
* Added for-each support to providers. ([#300](https://github.com/opentofu/opentofu/issues/300))
|
||||
|
||||
BUG FIXES:
|
||||
* Ensure that using a sensitive path for templatefile that it doesn't panic([#1801](https://github.com/opentofu/opentofu/issues/1801))
|
||||
|
@ -950,7 +950,6 @@ func (c *Config) transformProviderConfigsForTest(run *TestRun, file *TestFile) (
|
||||
// for by this run block.
|
||||
|
||||
for _, ref := range run.Providers {
|
||||
|
||||
testProvider, ok := file.getTestProviderOrMock(ref.InParent.String())
|
||||
if !ok {
|
||||
// Then this reference was invalid as we didn't have the
|
||||
@ -966,15 +965,16 @@ func (c *Config) transformProviderConfigsForTest(run *TestRun, file *TestFile) (
|
||||
}
|
||||
|
||||
next[ref.InChild.String()] = &Provider{
|
||||
Name: ref.InChild.Name,
|
||||
NameRange: ref.InChild.NameRange,
|
||||
Alias: ref.InChild.Alias,
|
||||
AliasRange: ref.InChild.AliasRange,
|
||||
Version: testProvider.Version,
|
||||
Config: testProvider.Config,
|
||||
DeclRange: testProvider.DeclRange,
|
||||
IsMocked: testProvider.IsMocked,
|
||||
MockResources: testProvider.MockResources,
|
||||
ProviderCommon: ProviderCommon{
|
||||
Name: ref.InChild.Name,
|
||||
NameRange: ref.InChild.NameRange,
|
||||
Version: testProvider.Version,
|
||||
Config: testProvider.Config,
|
||||
DeclRange: testProvider.DeclRange,
|
||||
IsMocked: testProvider.IsMocked,
|
||||
MockResources: testProvider.MockResources,
|
||||
},
|
||||
Alias: ref.InChild.Alias,
|
||||
}
|
||||
|
||||
}
|
||||
@ -986,13 +986,14 @@ func (c *Config) transformProviderConfigsForTest(run *TestRun, file *TestFile) (
|
||||
}
|
||||
for _, mp := range file.MockProviders {
|
||||
next[mp.moduleUniqueKey()] = &Provider{
|
||||
Name: mp.Name,
|
||||
NameRange: mp.NameRange,
|
||||
Alias: mp.Alias,
|
||||
AliasRange: mp.AliasRange,
|
||||
DeclRange: mp.DeclRange,
|
||||
IsMocked: true,
|
||||
MockResources: mp.MockResources,
|
||||
ProviderCommon: ProviderCommon{
|
||||
Name: mp.Name,
|
||||
NameRange: mp.NameRange,
|
||||
DeclRange: mp.DeclRange,
|
||||
IsMocked: true,
|
||||
MockResources: mp.MockResources,
|
||||
},
|
||||
Alias: mp.Alias,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -628,7 +628,9 @@ func TestTransformForTest(t *testing.T) {
|
||||
}
|
||||
|
||||
provider := &Provider{
|
||||
Config: file.Body,
|
||||
ProviderCommon: ProviderCommon{
|
||||
Config: file.Body,
|
||||
},
|
||||
}
|
||||
|
||||
parts := strings.Split(key, ".")
|
||||
|
@ -70,8 +70,8 @@ type Module struct {
|
||||
|
||||
// GetProviderConfig uses name and alias to find the respective Provider configuration.
|
||||
func (m *Module) GetProviderConfig(name, alias string) (*Provider, bool) {
|
||||
tp := &Provider{Name: name, Alias: alias}
|
||||
p, ok := m.ProviderConfigs[tp.moduleUniqueKey()]
|
||||
tp := &Provider{ProviderCommon: ProviderCommon{Name: name}, Alias: alias}
|
||||
p, ok := m.ProviderConfigs[tp.Addr().StringCompact()]
|
||||
return p, ok
|
||||
}
|
||||
|
||||
@ -93,7 +93,7 @@ type File struct {
|
||||
|
||||
Backends []*Backend
|
||||
CloudConfigs []*CloudConfig
|
||||
ProviderConfigs []*Provider
|
||||
ProviderConfigs []*ProviderBlock
|
||||
ProviderMetas []*ProviderMeta
|
||||
RequiredProviders []*RequiredProviders
|
||||
Encryptions []*config.EncryptionConfig
|
||||
@ -244,6 +244,17 @@ func NewModule(primaryFiles, overrideFiles []*File, call StaticModuleCall, sourc
|
||||
mod.CloudConfig.eval = mod.StaticEvaluator
|
||||
}
|
||||
|
||||
// Process all providers with the static context
|
||||
for _, file := range primaryFiles {
|
||||
fileDiags := mod.appendFileProviders(file)
|
||||
diags = append(diags, fileDiags...)
|
||||
}
|
||||
|
||||
for _, file := range overrideFiles {
|
||||
fileDiags := mod.mergeFileProviders(file)
|
||||
diags = append(diags, fileDiags...)
|
||||
}
|
||||
|
||||
// Process all module calls now that we have the static context
|
||||
for _, mc := range mod.ModuleCalls {
|
||||
mDiags := mc.decodeStaticFields(mod.StaticEvaluator)
|
||||
@ -317,29 +328,6 @@ func (m *Module) appendFile(file *File) hcl.Diagnostics {
|
||||
})
|
||||
}
|
||||
|
||||
for _, pc := range file.ProviderConfigs {
|
||||
key := pc.moduleUniqueKey()
|
||||
if existing, exists := m.ProviderConfigs[key]; exists {
|
||||
if existing.Alias == "" {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Duplicate provider configuration",
|
||||
Detail: fmt.Sprintf("A default (non-aliased) provider configuration for %q was already given at %s. If multiple configurations are required, set the \"alias\" argument for alternative configurations.", existing.Name, existing.DeclRange),
|
||||
Subject: &pc.DeclRange,
|
||||
})
|
||||
} else {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Duplicate provider configuration",
|
||||
Detail: fmt.Sprintf("A provider configuration for %q with alias %q was already given at %s. Each configuration for the same provider must have a distinct alias.", existing.Name, existing.Alias, existing.DeclRange),
|
||||
Subject: &pc.DeclRange,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
m.ProviderConfigs[key] = pc
|
||||
}
|
||||
|
||||
for _, pm := range file.ProviderMetas {
|
||||
provider := m.ProviderForLocalConfig(addrs.LocalProviderConfig{LocalName: pm.Provider})
|
||||
if existing, exists := m.ProviderMetas[provider]; exists {
|
||||
@ -590,38 +578,6 @@ func (m *Module) mergeFile(file *File) hcl.Diagnostics {
|
||||
}
|
||||
}
|
||||
|
||||
for _, pc := range file.ProviderConfigs {
|
||||
key := pc.moduleUniqueKey()
|
||||
existing, exists := m.ProviderConfigs[key]
|
||||
if pc.Alias == "" {
|
||||
// We allow overriding a non-existing _default_ provider configuration
|
||||
// because the user model is that an absent provider configuration
|
||||
// implies an empty provider configuration, which is what the user
|
||||
// is therefore overriding here.
|
||||
if exists {
|
||||
mergeDiags := existing.merge(pc)
|
||||
diags = append(diags, mergeDiags...)
|
||||
} else {
|
||||
m.ProviderConfigs[key] = pc
|
||||
}
|
||||
} else {
|
||||
// For aliased providers, there must be a base configuration to
|
||||
// override. This allows us to detect and report alias typos
|
||||
// that might otherwise cause the override to not apply.
|
||||
if !exists {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Missing base provider configuration for override",
|
||||
Detail: fmt.Sprintf("There is no %s provider configuration with the alias %q. An override file can only override an aliased provider configuration that was already defined in a primary configuration file.", pc.Name, pc.Alias),
|
||||
Subject: &pc.DeclRange,
|
||||
})
|
||||
continue
|
||||
}
|
||||
mergeDiags := existing.merge(pc)
|
||||
diags = append(diags, mergeDiags...)
|
||||
}
|
||||
}
|
||||
|
||||
if len(file.Encryptions) != 0 {
|
||||
switch len(file.Encryptions) {
|
||||
case 1:
|
||||
@ -760,6 +716,85 @@ func (m *Module) mergeFile(file *File) hcl.Diagnostics {
|
||||
return diags
|
||||
}
|
||||
|
||||
func (m *Module) appendFileProviders(file *File) hcl.Diagnostics {
|
||||
var diags hcl.Diagnostics
|
||||
for _, pci := range file.ProviderConfigs {
|
||||
pcs, decodeDiags := pci.decodeStaticFields(m.StaticEvaluator)
|
||||
diags = append(diags, decodeDiags...)
|
||||
if decodeDiags.HasErrors() {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, pc := range pcs {
|
||||
key := pc.Addr().StringCompact()
|
||||
if existing, exists := m.ProviderConfigs[key]; exists {
|
||||
if existing.Alias == "" {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Duplicate provider configuration",
|
||||
Detail: fmt.Sprintf("A default (non-aliased) provider configuration for %q was already given at %s. If multiple configurations are required, set the \"alias\" argument for alternative configurations.", existing.Name, existing.DeclRange),
|
||||
Subject: &pc.DeclRange,
|
||||
})
|
||||
} else {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Duplicate provider configuration",
|
||||
Detail: fmt.Sprintf("A provider configuration for %q with alias %q was already given at %s. Each configuration for the same provider must have a distinct alias.", existing.Name, existing.Alias, existing.DeclRange),
|
||||
Subject: &pc.DeclRange,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
m.ProviderConfigs[key] = pc
|
||||
}
|
||||
}
|
||||
return diags
|
||||
}
|
||||
|
||||
func (m *Module) mergeFileProviders(file *File) hcl.Diagnostics {
|
||||
var diags hcl.Diagnostics
|
||||
for _, pci := range file.ProviderConfigs {
|
||||
pcs, decodeDiags := pci.decodeStaticFields(m.StaticEvaluator)
|
||||
diags = append(diags, decodeDiags...)
|
||||
if decodeDiags.HasErrors() {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, pc := range pcs {
|
||||
key := pc.Addr().StringCompact()
|
||||
existing, exists := m.ProviderConfigs[key]
|
||||
if pc.Alias == "" {
|
||||
// We allow overriding a non-existing _default_ provider configuration
|
||||
// because the user model is that an absent provider configuration
|
||||
// implies an empty provider configuration, which is what the user
|
||||
// is therefore overriding here.
|
||||
if exists {
|
||||
mergeDiags := existing.merge(pc)
|
||||
diags = append(diags, mergeDiags...)
|
||||
} else {
|
||||
m.ProviderConfigs[key] = pc
|
||||
}
|
||||
} else {
|
||||
// For aliased providers, there must be a base configuration to
|
||||
// override. This allows us to detect and report alias typos
|
||||
// that might otherwise cause the override to not apply.
|
||||
if !exists {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Missing base provider configuration for override",
|
||||
Detail: fmt.Sprintf("There is no %s provider configuration with the alias %q. An override file can only override an aliased provider configuration that was already defined in a primary configuration file.", pc.Name, pc.Alias),
|
||||
Subject: &pc.DeclRange,
|
||||
})
|
||||
continue
|
||||
}
|
||||
mergeDiags := existing.merge(pc)
|
||||
diags = append(diags, mergeDiags...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return diags
|
||||
}
|
||||
|
||||
// gatherProviderLocalNames is a helper function that populatesA a map of
|
||||
// provider FQNs -> provider local names. This information is useful for
|
||||
// user-facing output, which should include both the FQN and LocalName. It must
|
||||
|
@ -13,17 +13,15 @@ import (
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/instances"
|
||||
"github.com/opentofu/opentofu/internal/lang/evalchecks"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
)
|
||||
|
||||
// Provider represents a "provider" block in a module or file. A provider
|
||||
// block is a provider configuration, and there can be zero or more
|
||||
// configurations for each actual provider.
|
||||
type Provider struct {
|
||||
Name string
|
||||
NameRange hcl.Range
|
||||
Alias string
|
||||
AliasRange *hcl.Range // nil if no alias set
|
||||
// ProviderCommon is the common fields between a Provider Block and a Provider
|
||||
type ProviderCommon struct {
|
||||
Name string
|
||||
NameRange hcl.Range
|
||||
|
||||
Version VersionConstraint
|
||||
|
||||
@ -44,7 +42,105 @@ type Provider struct {
|
||||
MockResources []*MockResource
|
||||
}
|
||||
|
||||
func decodeProviderBlock(block *hcl.Block) (*Provider, hcl.Diagnostics) {
|
||||
// ProviderBlock represents a "provider" block in a module or file. A provider
|
||||
// block is a provider configuration
|
||||
type ProviderBlock struct {
|
||||
ProviderCommon
|
||||
AliasExpr hcl.Expression // nil if no alias set
|
||||
AliasRange *hcl.Range // nil if no alias set
|
||||
ForEach hcl.Expression
|
||||
}
|
||||
|
||||
// Provider represents an instance of a "provider" block. Created after
|
||||
// the exvaluation of a provider block containing the specific data
|
||||
// for each instance derived from the evaluation
|
||||
type Provider struct {
|
||||
ProviderCommon
|
||||
Alias string
|
||||
InstanceData instances.RepetitionData
|
||||
}
|
||||
|
||||
// ParseProviderConfigCompact parses the given absolute traversal as a relative
|
||||
// provider address in compact form. The following are examples of traversals
|
||||
// that can be successfully parsed as compact relative provider configuration
|
||||
// addresses:
|
||||
//
|
||||
// - aws
|
||||
// - aws.foo
|
||||
//
|
||||
// This function will panic if given a relative traversal.
|
||||
//
|
||||
// If the returned diagnostics contains errors then the result value is invalid
|
||||
// and must not be used.
|
||||
func ParseProviderConfigCompact(traversal hcl.Traversal) (addrs.LocalProviderConfig, tfdiags.Diagnostics) {
|
||||
// added as a const to keep the linter happy
|
||||
const providerAddrMaxTraversal = 2
|
||||
var diags tfdiags.Diagnostics
|
||||
ret := addrs.LocalProviderConfig{
|
||||
LocalName: traversal.RootName(),
|
||||
}
|
||||
|
||||
if len(traversal) < providerAddrMaxTraversal {
|
||||
// Just a type name, then.
|
||||
return ret, diags
|
||||
}
|
||||
|
||||
aliasStep := traversal[1]
|
||||
switch ts := aliasStep.(type) {
|
||||
case hcl.TraverseAttr:
|
||||
ret.Alias = ts.Name
|
||||
return ret, diags
|
||||
default:
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid provider configuration address",
|
||||
Detail: "The provider type name must either stand alone or be followed by an alias name separated with a dot.",
|
||||
Subject: aliasStep.SourceRange().Ptr(),
|
||||
})
|
||||
}
|
||||
|
||||
if len(traversal) > providerAddrMaxTraversal {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid provider configuration address",
|
||||
Detail: "Extraneous extra operators after provider configuration address.",
|
||||
Subject: traversal[providerAddrMaxTraversal:].SourceRange().Ptr(),
|
||||
})
|
||||
}
|
||||
|
||||
return ret, diags
|
||||
}
|
||||
|
||||
// ParseProviderConfigCompactStr is a helper wrapper around ParseProviderConfigCompact
|
||||
// that takes a string and parses it with the HCL native syntax traversal parser
|
||||
// before interpreting it.
|
||||
//
|
||||
// This should be used only in specialized situations since it will cause the
|
||||
// created references to not have any meaningful source location information.
|
||||
// If a reference string is coming from a source that should be identified in
|
||||
// error messages then the caller should instead parse it directly using a
|
||||
// suitable function from the HCL API and pass the traversal itself to
|
||||
// ParseProviderConfigCompact.
|
||||
//
|
||||
// Error diagnostics are returned if either the parsing fails or the analysis
|
||||
// of the traversal fails. There is no way for the caller to distinguish the
|
||||
// two kinds of diagnostics programmatically. If error diagnostics are returned
|
||||
// then the returned address is invalid.
|
||||
func ParseProviderConfigCompactStr(str string) (addrs.LocalProviderConfig, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1})
|
||||
diags = diags.Append(parseDiags)
|
||||
if parseDiags.HasErrors() {
|
||||
return addrs.LocalProviderConfig{}, diags
|
||||
}
|
||||
|
||||
addr, addrDiags := ParseProviderConfigCompact(traversal)
|
||||
diags = diags.Append(addrDiags)
|
||||
return addr, diags
|
||||
}
|
||||
|
||||
func decodeProviderBlock(block *hcl.Block) (*ProviderBlock, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
content, config, moreDiags := block.Body.PartialContent(providerBlockSchema)
|
||||
@ -62,25 +158,31 @@ func decodeProviderBlock(block *hcl.Block) (*Provider, hcl.Diagnostics) {
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
provider := &Provider{
|
||||
Name: name,
|
||||
NameRange: block.LabelRanges[0],
|
||||
Config: config,
|
||||
DeclRange: block.DefRange,
|
||||
provider := &ProviderBlock{
|
||||
ProviderCommon: ProviderCommon{
|
||||
Name: name,
|
||||
NameRange: block.LabelRanges[0],
|
||||
Config: config,
|
||||
DeclRange: block.DefRange,
|
||||
},
|
||||
}
|
||||
|
||||
if attr, exists := content.Attributes["alias"]; exists {
|
||||
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &provider.Alias)
|
||||
diags = append(diags, valDiags...)
|
||||
provider.AliasExpr = attr.Expr
|
||||
provider.AliasRange = attr.Expr.Range().Ptr()
|
||||
}
|
||||
|
||||
if !hclsyntax.ValidIdentifier(provider.Alias) {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid provider configuration alias",
|
||||
Detail: fmt.Sprintf("An alias must be a valid name. %s", badIdentifierDetail),
|
||||
})
|
||||
}
|
||||
if attr, exists := content.Attributes["for_each"]; exists {
|
||||
provider.ForEach = attr.Expr
|
||||
}
|
||||
|
||||
if provider.AliasExpr != nil && provider.ForEach != nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: `Invalid combination of "alias" and "for_each"`,
|
||||
Detail: `The "alias" and "for_each" arguments are mutually-exclusive, only one may be used.`,
|
||||
Subject: provider.AliasExpr.Range().Ptr(),
|
||||
})
|
||||
}
|
||||
|
||||
if attr, exists := content.Attributes["version"]; exists {
|
||||
@ -95,17 +197,8 @@ func decodeProviderBlock(block *hcl.Block) (*Provider, hcl.Diagnostics) {
|
||||
diags = append(diags, versionDiags...)
|
||||
}
|
||||
|
||||
// Reserved attribute names
|
||||
for _, name := range []string{"count", "depends_on", "for_each", "source"} {
|
||||
if attr, exists := content.Attributes[name]; exists {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Reserved argument name in provider block",
|
||||
Detail: fmt.Sprintf("The provider argument name %q is reserved for use by OpenTofu in a future version.", name),
|
||||
Subject: &attr.NameRange,
|
||||
})
|
||||
}
|
||||
}
|
||||
reserveredDiags := checkReservedNames(content)
|
||||
diags = append(diags, reserveredDiags...)
|
||||
|
||||
var seenEscapeBlock *hcl.Block
|
||||
for _, block := range content.Blocks {
|
||||
@ -145,122 +238,20 @@ func decodeProviderBlock(block *hcl.Block) (*Provider, hcl.Diagnostics) {
|
||||
return provider, diags
|
||||
}
|
||||
|
||||
// Addr returns the address of the receiving provider configuration, relative
|
||||
// to its containing module.
|
||||
func (p *Provider) Addr() addrs.LocalProviderConfig {
|
||||
return addrs.LocalProviderConfig{
|
||||
LocalName: p.Name,
|
||||
Alias: p.Alias,
|
||||
func checkReservedNames(content *hcl.BodyContent) hcl.Diagnostics {
|
||||
var diags hcl.Diagnostics
|
||||
// Reserved attribute names
|
||||
for _, name := range []string{"depends_on", "source", "count"} {
|
||||
if attr, exists := content.Attributes[name]; exists {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Reserved argument name in provider block",
|
||||
Detail: fmt.Sprintf("The provider argument name %q is reserved for use by OpenTofu in a future version.", name),
|
||||
Subject: &attr.NameRange,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) moduleUniqueKey() string {
|
||||
if p.Alias != "" {
|
||||
return fmt.Sprintf("%s.%s", p.Name, p.Alias)
|
||||
}
|
||||
return p.Name
|
||||
}
|
||||
|
||||
// ParseProviderConfigCompact parses the given absolute traversal as a relative
|
||||
// provider address in compact form. The following are examples of traversals
|
||||
// that can be successfully parsed as compact relative provider configuration
|
||||
// addresses:
|
||||
//
|
||||
// - aws
|
||||
// - aws.foo
|
||||
//
|
||||
// This function will panic if given a relative traversal.
|
||||
//
|
||||
// If the returned diagnostics contains errors then the result value is invalid
|
||||
// and must not be used.
|
||||
func ParseProviderConfigCompact(traversal hcl.Traversal) (addrs.LocalProviderConfig, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
ret := addrs.LocalProviderConfig{
|
||||
LocalName: traversal.RootName(),
|
||||
}
|
||||
|
||||
if len(traversal) < 2 {
|
||||
// Just a type name, then.
|
||||
return ret, diags
|
||||
}
|
||||
|
||||
aliasStep := traversal[1]
|
||||
switch ts := aliasStep.(type) {
|
||||
case hcl.TraverseAttr:
|
||||
ret.Alias = ts.Name
|
||||
return ret, diags
|
||||
default:
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid provider configuration address",
|
||||
Detail: "The provider type name must either stand alone or be followed by an alias name separated with a dot.",
|
||||
Subject: aliasStep.SourceRange().Ptr(),
|
||||
})
|
||||
}
|
||||
|
||||
if len(traversal) > 2 {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid provider configuration address",
|
||||
Detail: "Extraneous extra operators after provider configuration address.",
|
||||
Subject: traversal[2:].SourceRange().Ptr(),
|
||||
})
|
||||
}
|
||||
|
||||
return ret, diags
|
||||
}
|
||||
|
||||
// ParseProviderConfigCompactStr is a helper wrapper around ParseProviderConfigCompact
|
||||
// that takes a string and parses it with the HCL native syntax traversal parser
|
||||
// before interpreting it.
|
||||
//
|
||||
// This should be used only in specialized situations since it will cause the
|
||||
// created references to not have any meaningful source location information.
|
||||
// If a reference string is coming from a source that should be identified in
|
||||
// error messages then the caller should instead parse it directly using a
|
||||
// suitable function from the HCL API and pass the traversal itself to
|
||||
// ParseProviderConfigCompact.
|
||||
//
|
||||
// Error diagnostics are returned if either the parsing fails or the analysis
|
||||
// of the traversal fails. There is no way for the caller to distinguish the
|
||||
// two kinds of diagnostics programmatically. If error diagnostics are returned
|
||||
// then the returned address is invalid.
|
||||
func ParseProviderConfigCompactStr(str string) (addrs.LocalProviderConfig, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1})
|
||||
diags = diags.Append(parseDiags)
|
||||
if parseDiags.HasErrors() {
|
||||
return addrs.LocalProviderConfig{}, diags
|
||||
}
|
||||
|
||||
addr, addrDiags := ParseProviderConfigCompact(traversal)
|
||||
diags = diags.Append(addrDiags)
|
||||
return addr, diags
|
||||
}
|
||||
|
||||
var providerBlockSchema = &hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: "alias",
|
||||
},
|
||||
{
|
||||
Name: "version",
|
||||
},
|
||||
|
||||
// Attribute names reserved for future expansion.
|
||||
{Name: "count"},
|
||||
{Name: "depends_on"},
|
||||
{Name: "for_each"},
|
||||
{Name: "source"},
|
||||
},
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{Type: "_"}, // meta-argument escaping block
|
||||
|
||||
// The rest of these are reserved for future expansion.
|
||||
{Type: "lifecycle"},
|
||||
{Type: "locals"},
|
||||
},
|
||||
return diags
|
||||
}
|
||||
|
||||
// checkProviderNameNormalized verifies that the given string is already
|
||||
@ -290,3 +281,124 @@ func checkProviderNameNormalized(name string, declrange hcl.Range) hcl.Diagnosti
|
||||
}
|
||||
return diags
|
||||
}
|
||||
|
||||
// Addr returns the address of the receiving provider configuration, relative
|
||||
// to its containing module.
|
||||
func (p *Provider) Addr() addrs.LocalProviderConfig {
|
||||
return addrs.LocalProviderConfig{
|
||||
LocalName: p.Name,
|
||||
Alias: p.Alias,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ProviderBlock) decodeStaticFields(eval *StaticEvaluator) ([]*Provider, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
if p.ForEach != nil {
|
||||
return p.generateForEachProviders(eval)
|
||||
}
|
||||
|
||||
result := Provider{ProviderCommon: p.ProviderCommon}
|
||||
if p.AliasExpr != nil {
|
||||
if eval != nil {
|
||||
valDiags := eval.DecodeExpression(p.AliasExpr, StaticIdentifier{
|
||||
Module: eval.call.addr,
|
||||
Subject: fmt.Sprintf("provider.%s.alias", p.Name),
|
||||
DeclRange: p.AliasExpr.Range(),
|
||||
}, &result.Alias)
|
||||
diags = append(diags, valDiags...)
|
||||
} else {
|
||||
// Test files don't have a static context
|
||||
valDiags := gohcl.DecodeExpression(p.AliasExpr, nil, &result.Alias)
|
||||
diags = append(diags, valDiags...)
|
||||
}
|
||||
|
||||
if diags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
if !hclsyntax.ValidIdentifier(result.Alias) {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid provider configuration alias",
|
||||
Detail: fmt.Sprintf("Alias %q must be a valid name. %s", result.Alias, badIdentifierDetail),
|
||||
Subject: p.AliasExpr.Range().Ptr(),
|
||||
})
|
||||
}
|
||||
}
|
||||
return []*Provider{&result}, diags
|
||||
}
|
||||
|
||||
func (p *ProviderBlock) generateForEachProviders(eval *StaticEvaluator) ([]*Provider, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
if eval == nil {
|
||||
return nil, diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Iteration not allowed in test files",
|
||||
Detail: "for_each was declared as a provider attribute in a test file",
|
||||
Subject: p.ForEach.Range().Ptr(),
|
||||
})
|
||||
}
|
||||
|
||||
forEachRefsFunc := func(refs []*addrs.Reference) (*hcl.EvalContext, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
evalContext, evalDiags := eval.EvalContext(StaticIdentifier{
|
||||
Module: eval.call.addr,
|
||||
Subject: fmt.Sprintf("provider.%s.for_each", p.Name),
|
||||
DeclRange: p.ForEach.Range(),
|
||||
}, refs)
|
||||
return evalContext, diags.Append(evalDiags)
|
||||
}
|
||||
|
||||
forVal, evalDiags := evalchecks.EvaluateForEachExpression(p.ForEach, forEachRefsFunc)
|
||||
diags = append(diags, evalDiags.ToHCL()...)
|
||||
if evalDiags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
var out []*Provider
|
||||
for k, v := range forVal {
|
||||
if !hclsyntax.ValidIdentifier(k) {
|
||||
return nil, diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid for_each key alias",
|
||||
Detail: fmt.Sprintf("Alias %q must be a valid name. %s", k, badIdentifierDetail),
|
||||
Subject: p.ForEach.Range().Ptr(),
|
||||
})
|
||||
}
|
||||
|
||||
out = append(out, &Provider{
|
||||
ProviderCommon: p.ProviderCommon,
|
||||
Alias: k,
|
||||
InstanceData: instances.RepetitionData{
|
||||
EachValue: v,
|
||||
},
|
||||
})
|
||||
}
|
||||
return out, diags
|
||||
}
|
||||
|
||||
var providerBlockSchema = &hcl.BodySchema{ //nolint: gochecknoglobals // pre-existing code
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: "alias",
|
||||
},
|
||||
{
|
||||
Name: "version",
|
||||
},
|
||||
{
|
||||
Name: "for_each",
|
||||
},
|
||||
|
||||
// Attribute names reserved for future expansion.
|
||||
{Name: "count"},
|
||||
{Name: "depends_on"},
|
||||
{Name: "source"},
|
||||
},
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{Type: "_"}, // meta-argument escaping block
|
||||
|
||||
// The rest of these are reserved for future expansion.
|
||||
{Type: "lifecycle"},
|
||||
{Type: "locals"},
|
||||
},
|
||||
}
|
||||
|
76
internal/configs/provider_iteration_test.go
Normal file
76
internal/configs/provider_iteration_test.go
Normal file
@ -0,0 +1,76 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package configs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
)
|
||||
|
||||
const (
|
||||
providerTestName = "local"
|
||||
)
|
||||
|
||||
func TestNewModule_provider_foreach(t *testing.T) {
|
||||
mod, diags := testModuleFromDir("testdata/providers_foreach")
|
||||
if diags.HasErrors() {
|
||||
t.Fatal(diags.Error())
|
||||
}
|
||||
|
||||
p := addrs.NewProvider(addrs.DefaultProviderRegistryHost, "hashicorp", providerTestName)
|
||||
if name, exists := mod.ProviderLocalNames[p]; !exists {
|
||||
t.Fatal("provider FQN hashicorp/local not found")
|
||||
} else if name != providerTestName {
|
||||
t.Fatalf("provider localname mismatch: got %s, want %s", name, providerTestName)
|
||||
}
|
||||
|
||||
if len(mod.ProviderConfigs) != 3 {
|
||||
t.Fatalf("incorrect number of providers: got %d, expected: %d", len(mod.ProviderConfigs), 3)
|
||||
}
|
||||
|
||||
_, foundDev := mod.GetProviderConfig("foo-test", "dev")
|
||||
if !foundDev {
|
||||
t.Fatal("unable to find dev provider")
|
||||
}
|
||||
|
||||
_, foundTest := mod.GetProviderConfig("foo-test", "test")
|
||||
if !foundTest {
|
||||
t.Fatal("unable to find test provider")
|
||||
}
|
||||
|
||||
_, foundProd := mod.GetProviderConfig("foo-test", "prod")
|
||||
if !foundProd {
|
||||
t.Fatal("unable to find prod provider")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewModule_provider_invalid_name(t *testing.T) {
|
||||
mod, diags := testModuleFromDir("testdata/providers_iteration_invalid_name")
|
||||
if !diags.HasErrors() {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
expected := "Invalid for_each key alias"
|
||||
expectedDetail := "Alias \"0\" must be a valid name. A name must start with a letter or underscore and may contain only letters, digits, underscores, and dashes."
|
||||
|
||||
if gotErr := diags[0].Summary; gotErr != expected {
|
||||
t.Errorf("wrong error, got %q, want %q", gotErr, expected)
|
||||
}
|
||||
if gotErr := diags[0].Detail; gotErr != expectedDetail {
|
||||
t.Errorf("wrong error, got %q, want %q", gotErr, expectedDetail)
|
||||
}
|
||||
|
||||
p := addrs.NewProvider(addrs.DefaultProviderRegistryHost, "hashicorp", providerTestName)
|
||||
if name, exists := mod.ProviderLocalNames[p]; !exists {
|
||||
t.Fatal("provider FQN hashicorp/local not found")
|
||||
} else if name != providerTestName {
|
||||
t.Fatalf("provider localname mismatch: got %s, want %s", name, providerTestName)
|
||||
}
|
||||
|
||||
if len(mod.ProviderConfigs) != 0 {
|
||||
t.Fatalf("incorrect number of providers: got %d, expected: %d", len(mod.ProviderConfigs), 0)
|
||||
}
|
||||
}
|
@ -30,10 +30,10 @@ func TestProviderReservedNames(t *testing.T) {
|
||||
`config.tf:4,13-20: Version constraints inside provider configuration blocks are deprecated; OpenTofu 0.13 and earlier allowed provider version constraints inside the provider configuration block, but that is now deprecated and will be removed in a future version of OpenTofu. To silence this warning, move the provider version constraint into the required_providers block.`,
|
||||
`config.tf:10,3-8: Reserved argument name in provider block; The provider argument name "count" is reserved for use by OpenTofu in a future version.`,
|
||||
`config.tf:11,3-13: Reserved argument name in provider block; The provider argument name "depends_on" is reserved for use by OpenTofu in a future version.`,
|
||||
`config.tf:12,3-11: Reserved argument name in provider block; The provider argument name "for_each" is reserved for use by OpenTofu in a future version.`,
|
||||
`config.tf:14,3-12: Reserved block type name in provider block; The block type name "lifecycle" is reserved for use by OpenTofu in a future version.`,
|
||||
`config.tf:15,3-9: Reserved block type name in provider block; The block type name "locals" is reserved for use by OpenTofu in a future version.`,
|
||||
`config.tf:13,3-9: Reserved argument name in provider block; The provider argument name "source" is reserved for use by OpenTofu in a future version.`,
|
||||
`config.tf:3,13-18: Invalid combination of "alias" and "for_each"; The "alias" and "for_each" arguments are mutually-exclusive, only one may be used.`,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -94,7 +94,7 @@ func validateProviderConfigsForTests(cfg *Config) (diags hcl.Diagnostics) {
|
||||
Summary: "Provider type mismatch",
|
||||
Detail: fmt.Sprintf(
|
||||
"The provider %q in %s represents provider %q, but %q in the root module represents %q.\n\nThis means the provider definition for %q within %s, or other provider definitions with the same name, have been referenced by multiple run blocks and assigned to different provider types.",
|
||||
provider.moduleUniqueKey(), name, providerType, requirement.Name, requirement.Type, provider.moduleUniqueKey(), name),
|
||||
provider.Addr().StringCompact(), name, providerType, requirement.Name, requirement.Type, provider.Addr().StringCompact(), name),
|
||||
Subject: provider.DeclRange.Ptr(),
|
||||
})
|
||||
}
|
||||
@ -114,7 +114,7 @@ func validateProviderConfigsForTests(cfg *Config) (diags hcl.Diagnostics) {
|
||||
Summary: "Provider type mismatch",
|
||||
Detail: fmt.Sprintf(
|
||||
"The provider %q in %s represents provider %q, but %q in the root module represents %q.\n\nThis means the provider definition for %q within %s, or other provider definitions with the same name, have been referenced by multiple run blocks and assigned to different provider types.",
|
||||
provider.moduleUniqueKey(), name, providerType, alias.StringCompact(), requirement.Type, provider.moduleUniqueKey(), name),
|
||||
provider.Addr().StringCompact(), name, providerType, alias.StringCompact(), requirement.Type, provider.Addr().StringCompact(), name),
|
||||
Subject: provider.DeclRange.Ptr(),
|
||||
})
|
||||
}
|
||||
@ -129,8 +129,7 @@ func validateProviderConfigsForTests(cfg *Config) (diags hcl.Diagnostics) {
|
||||
providerType = addrs.NewDefaultProvider(provider.Name)
|
||||
}
|
||||
|
||||
if testProvider, exists := test.Providers[provider.moduleUniqueKey()]; exists {
|
||||
|
||||
if testProvider, exists := test.Providers[provider.Addr().StringCompact()]; exists {
|
||||
testProviderType := testProvider.providerType
|
||||
if testProviderType.IsZero() {
|
||||
testProviderType = addrs.NewDefaultProvider(testProvider.Name)
|
||||
@ -142,7 +141,7 @@ func validateProviderConfigsForTests(cfg *Config) (diags hcl.Diagnostics) {
|
||||
Summary: "Provider type mismatch",
|
||||
Detail: fmt.Sprintf(
|
||||
"The provider %q in %s represents provider %q, but %q in the root module represents %q.\n\nThis means the provider definition for %q within %s has been referenced by multiple run blocks and assigned to different provider types.",
|
||||
testProvider.moduleUniqueKey(), name, testProviderType, provider.moduleUniqueKey(), providerType, testProvider.moduleUniqueKey(), name),
|
||||
testProvider.Addr().StringCompact(), name, testProviderType, provider.Addr().StringCompact(), providerType, testProvider.Addr().StringCompact(), name),
|
||||
Subject: testProvider.DeclRange.Ptr(),
|
||||
})
|
||||
}
|
||||
@ -178,7 +177,6 @@ func validateProviderConfigsForTests(cfg *Config) (diags hcl.Diagnostics) {
|
||||
Name: provider.Name,
|
||||
NameRange: provider.NameRange,
|
||||
Alias: provider.Alias,
|
||||
AliasRange: provider.AliasRange,
|
||||
providerType: provider.providerType,
|
||||
},
|
||||
}
|
||||
@ -203,7 +201,6 @@ func validateProviderConfigsForTests(cfg *Config) (diags hcl.Diagnostics) {
|
||||
Name: provider.Name,
|
||||
NameRange: provider.NameRange,
|
||||
Alias: provider.Alias,
|
||||
AliasRange: provider.AliasRange,
|
||||
providerType: provider.providerType,
|
||||
},
|
||||
}
|
||||
@ -223,7 +220,7 @@ func validateProviderConfigsForTests(cfg *Config) (diags hcl.Diagnostics) {
|
||||
// better error messages to use these.
|
||||
|
||||
for _, provider := range cfg.Module.ProviderConfigs {
|
||||
key := provider.moduleUniqueKey()
|
||||
key := provider.Addr().StringCompact()
|
||||
|
||||
if testProvider, exists := test.Providers[key]; exists {
|
||||
matchedProviders[key] = PassedProviderConfig{
|
||||
@ -238,7 +235,6 @@ func validateProviderConfigsForTests(cfg *Config) (diags hcl.Diagnostics) {
|
||||
Name: testProvider.Name,
|
||||
NameRange: testProvider.NameRange,
|
||||
Alias: testProvider.Alias,
|
||||
AliasRange: testProvider.AliasRange,
|
||||
providerType: testProvider.providerType,
|
||||
},
|
||||
}
|
||||
|
@ -100,13 +100,14 @@ func (file *TestFile) getTestProviderOrMock(addr string) (*Provider, bool) {
|
||||
mockProvider, ok := file.MockProviders[addr]
|
||||
if ok {
|
||||
p := &Provider{
|
||||
Name: mockProvider.Name,
|
||||
NameRange: mockProvider.NameRange,
|
||||
Alias: mockProvider.Alias,
|
||||
AliasRange: mockProvider.AliasRange,
|
||||
DeclRange: mockProvider.DeclRange,
|
||||
IsMocked: true,
|
||||
MockResources: mockProvider.MockResources,
|
||||
ProviderCommon: ProviderCommon{
|
||||
Name: mockProvider.Name,
|
||||
NameRange: mockProvider.NameRange,
|
||||
DeclRange: mockProvider.DeclRange,
|
||||
IsMocked: true,
|
||||
MockResources: mockProvider.MockResources,
|
||||
},
|
||||
Alias: mockProvider.Alias,
|
||||
}
|
||||
|
||||
return p, true
|
||||
@ -396,10 +397,17 @@ func loadTestFile(body hcl.Body) (*TestFile, hcl.Diagnostics) {
|
||||
}
|
||||
|
||||
case "provider":
|
||||
provider, providerDiags := decodeProviderBlock(block)
|
||||
providerBlock, providerDiags := decodeProviderBlock(block)
|
||||
diags = append(diags, providerDiags...)
|
||||
if provider != nil {
|
||||
tf.Providers[provider.moduleUniqueKey()] = provider
|
||||
if providerBlock != nil {
|
||||
providers, diagsStatic := providerBlock.decodeStaticFields(nil)
|
||||
diags = append(diags, diagsStatic...)
|
||||
if diagsStatic.HasErrors() {
|
||||
continue
|
||||
}
|
||||
for _, provider := range providers {
|
||||
tf.Providers[provider.Addr().StringCompact()] = provider
|
||||
}
|
||||
}
|
||||
|
||||
case blockNameOverrideResource:
|
||||
|
28
internal/configs/testdata/providers_foreach/root.tf
vendored
Normal file
28
internal/configs/testdata/providers_foreach/root.tf
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
local = {
|
||||
source = "hashicorp/local"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
locals {
|
||||
files = {
|
||||
dev = {
|
||||
filename = "/tmp/dev"
|
||||
content = "who dis?"
|
||||
}
|
||||
test = {
|
||||
filename = "/tmp/test"
|
||||
content = "testing 1 2 3"
|
||||
}
|
||||
prod = {
|
||||
filename = "/tmp/prod"
|
||||
content = "this is a serious string, because it's production"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "foo-test" {
|
||||
for_each = local.files
|
||||
}
|
28
internal/configs/testdata/providers_iteration_invalid_name/root.tf
vendored
Normal file
28
internal/configs/testdata/providers_iteration_invalid_name/root.tf
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
local = {
|
||||
source = "hashicorp/local"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
locals {
|
||||
files = {
|
||||
0 = {
|
||||
filename = "/tmp/0"
|
||||
content = "who dis?"
|
||||
}
|
||||
test = {
|
||||
filename = "/tmp/test"
|
||||
content = "testing 1 2 3"
|
||||
}
|
||||
prod = {
|
||||
filename = "/tmp/prod"
|
||||
content = "this is a serious string, because it's production"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "foo-test" {
|
||||
for_each = local.files
|
||||
}
|
@ -11,3 +11,7 @@ provider "bar" {
|
||||
|
||||
alias = "bar"
|
||||
}
|
||||
|
||||
provider "baz" {
|
||||
for_each = {"a": "first", "b": "second"}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package tofu
|
||||
package evalchecks
|
||||
|
||||
import (
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
@ -21,11 +21,11 @@ import (
|
||||
// populate the EvalContext and Expression fields of the diagnostic so that
|
||||
// the diagnostic renderer can use all of that information together to assist
|
||||
// the user in understanding what was unknown.
|
||||
type diagnosticCausedByUnknown bool
|
||||
type DiagnosticCausedByUnknown bool
|
||||
|
||||
var _ tfdiags.DiagnosticExtraBecauseUnknown = diagnosticCausedByUnknown(true)
|
||||
var _ tfdiags.DiagnosticExtraBecauseUnknown = DiagnosticCausedByUnknown(true)
|
||||
|
||||
func (e diagnosticCausedByUnknown) DiagnosticCausedByUnknown() bool {
|
||||
func (e DiagnosticCausedByUnknown) DiagnosticCausedByUnknown() bool {
|
||||
return bool(e)
|
||||
}
|
||||
|
||||
@ -38,10 +38,10 @@ func (e diagnosticCausedByUnknown) DiagnosticCausedByUnknown() bool {
|
||||
// populate the EvalContext and Expression fields of the diagnostic so that
|
||||
// the diagnostic renderer can use all of that information together to assist
|
||||
// the user in understanding what was sensitive.
|
||||
type diagnosticCausedBySensitive bool
|
||||
type DiagnosticCausedBySensitive bool
|
||||
|
||||
var _ tfdiags.DiagnosticExtraBecauseSensitive = diagnosticCausedBySensitive(true)
|
||||
var _ tfdiags.DiagnosticExtraBecauseSensitive = DiagnosticCausedBySensitive(true)
|
||||
|
||||
func (e diagnosticCausedBySensitive) DiagnosticCausedBySensitive() bool {
|
||||
func (e DiagnosticCausedBySensitive) DiagnosticCausedBySensitive() bool {
|
||||
return bool(e)
|
||||
}
|
@ -2,8 +2,7 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package tofu
|
||||
package evalchecks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -14,16 +13,18 @@ import (
|
||||
"github.com/zclconf/go-cty/cty/gocty"
|
||||
)
|
||||
|
||||
// evaluateCountExpression is our standard mechanism for interpreting an
|
||||
type EvaluateFunc func(expr hcl.Expression) (cty.Value, tfdiags.Diagnostics)
|
||||
|
||||
// EvaluateCountExpression is our standard mechanism for interpreting an
|
||||
// expression given for a "count" argument on a resource or a module. This
|
||||
// should be called during expansion in order to determine the final count
|
||||
// value.
|
||||
//
|
||||
// evaluateCountExpression differs from evaluateCountExpressionValue by
|
||||
// EvaluateCountExpression differs from EvaluateCountExpressionValue by
|
||||
// returning an error if the count value is not known, and converting the
|
||||
// cty.Value to an integer.
|
||||
func evaluateCountExpression(expr hcl.Expression, ctx EvalContext) (int, tfdiags.Diagnostics) {
|
||||
countVal, diags := evaluateCountExpressionValue(expr, ctx)
|
||||
func EvaluateCountExpression(expr hcl.Expression, ctx EvaluateFunc) (int, tfdiags.Diagnostics) {
|
||||
countVal, diags := EvaluateCountExpressionValue(expr, ctx)
|
||||
if !countVal.IsKnown() {
|
||||
// Currently this is a rather bad outcome from a UX standpoint, since we have
|
||||
// no real mechanism to deal with this situation and all we can do is produce
|
||||
@ -41,7 +42,7 @@ func evaluateCountExpression(expr hcl.Expression, ctx EvalContext) (int, tfdiags
|
||||
// we can't easily do that right now because the hcl.EvalContext
|
||||
// (which is not the same as the ctx we have in scope here) is
|
||||
// hidden away inside evaluateCountExpressionValue.
|
||||
Extra: diagnosticCausedByUnknown(true),
|
||||
Extra: DiagnosticCausedByUnknown(true),
|
||||
})
|
||||
}
|
||||
|
||||
@ -53,17 +54,17 @@ func evaluateCountExpression(expr hcl.Expression, ctx EvalContext) (int, tfdiags
|
||||
return int(count), diags
|
||||
}
|
||||
|
||||
// evaluateCountExpressionValue is like evaluateCountExpression
|
||||
// EvaluateCountExpressionValue is like EvaluateCountExpression
|
||||
// except that it returns a cty.Value which must be a cty.Number and can be
|
||||
// unknown.
|
||||
func evaluateCountExpressionValue(expr hcl.Expression, ctx EvalContext) (cty.Value, tfdiags.Diagnostics) {
|
||||
func EvaluateCountExpressionValue(expr hcl.Expression, ctx EvaluateFunc) (cty.Value, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
nullCount := cty.NullVal(cty.Number)
|
||||
if expr == nil {
|
||||
return nullCount, nil
|
||||
}
|
||||
|
||||
countVal, countDiags := ctx.EvaluateExpr(expr, cty.Number, nil)
|
||||
countVal, countDiags := ctx(expr)
|
||||
diags = diags.Append(countDiags)
|
||||
if diags.HasErrors() {
|
||||
return nullCount, diags
|
121
internal/lang/evalchecks/eval_count_test.go
Normal file
121
internal/lang/evalchecks/eval_count_test.go
Normal file
@ -0,0 +1,121 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
package evalchecks
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hcltest"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// returns a mock ref function for the unit tests
|
||||
func mockEvaluateFunc(val cty.Value) EvaluateFunc {
|
||||
return func(_ hcl.Expression) (cty.Value, tfdiags.Diagnostics) {
|
||||
return val, tfdiags.Diagnostics{}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateCountExpression_valid(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
val cty.Value
|
||||
expected int
|
||||
}{
|
||||
"1": {
|
||||
cty.NumberIntVal(1),
|
||||
1,
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
actual, diags := EvaluateCountExpression(hcltest.MockExprLiteral(test.val), mockEvaluateFunc(test.val))
|
||||
|
||||
if len(diags) != 0 {
|
||||
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(actual, test.expected) {
|
||||
t.Errorf(
|
||||
"wrong map value\ngot: %swant: %s",
|
||||
spew.Sdump(actual), spew.Sdump(test.expected),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateCountExpression_errors(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
val cty.Value
|
||||
Summary, DetailSubstring string
|
||||
CausedByUnknown bool
|
||||
}{
|
||||
"null": {
|
||||
cty.NullVal(cty.Number),
|
||||
"Invalid count argument",
|
||||
`The given "count" argument value is null. An integer is required.`,
|
||||
false,
|
||||
},
|
||||
"negative": {
|
||||
cty.NumberIntVal(-1),
|
||||
"Invalid count argument",
|
||||
`The given "count" argument value is unsuitable: must be greater than or equal to zero.`,
|
||||
false,
|
||||
},
|
||||
"string": {
|
||||
cty.StringVal("i am definitely a number"),
|
||||
"Invalid count argument",
|
||||
"The given \"count\" argument value is unsuitable: number value is required.",
|
||||
false,
|
||||
},
|
||||
"list": {
|
||||
cty.ListVal([]cty.Value{cty.StringVal("a"), cty.StringVal("a")}),
|
||||
"Invalid count argument",
|
||||
"The given \"count\" argument value is unsuitable: number value is required.",
|
||||
false,
|
||||
},
|
||||
"tuple": {
|
||||
cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")}),
|
||||
"Invalid count argument",
|
||||
"The given \"count\" argument value is unsuitable: number value is required.",
|
||||
false,
|
||||
},
|
||||
"unknown": {
|
||||
cty.UnknownVal(cty.Number),
|
||||
"Invalid count argument",
|
||||
`he "count" value depends on resource attributes that cannot be determined until apply, so OpenTofu cannot predict how many instances will be created. To work around this, use the -target argument to first apply only the resources that the count depends on.`,
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
_, diags := EvaluateCountExpression(hcltest.MockExprLiteral(test.val), mockEvaluateFunc(test.val))
|
||||
|
||||
if len(diags) != 1 {
|
||||
t.Fatalf("got %d diagnostics; want 1", diags)
|
||||
}
|
||||
if got, want := diags[0].Severity(), tfdiags.Error; got != want {
|
||||
t.Errorf("wrong diagnostic severity %#v; want %#v", got, want)
|
||||
}
|
||||
if got, want := diags[0].Description().Summary, test.Summary; got != want {
|
||||
t.Errorf("wrong diagnostic summary\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := diags[0].Description().Detail, test.DetailSubstring; !strings.Contains(got, want) {
|
||||
t.Errorf("wrong diagnostic detail\ngot: %s\nwant substring: %s", got, want)
|
||||
}
|
||||
|
||||
if got, want := tfdiags.DiagnosticCausedByUnknown(diags[0]), test.CausedByUnknown; got != want {
|
||||
t.Errorf("wrong result from tfdiags.DiagnosticCausedByUnknown\ngot: %#v\nwant: %#v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -2,33 +2,38 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package tofu
|
||||
package evalchecks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/lang"
|
||||
"github.com/opentofu/opentofu/internal/lang/marks"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// evaluateForEachExpression is our standard mechanism for interpreting an
|
||||
const (
|
||||
errInvalidUnknownDetailMap = "The \"for_each\" map includes keys derived from resource attributes that cannot be determined until apply, and so OpenTofu cannot determine the full set of keys that will identify the instances of this resource.\n\nWhen working with unknown values in for_each, it's better to define the map keys statically in your configuration and place apply-time results only in the map values.\n\nAlternatively, you could use the -target planning option to first apply only the resources that the for_each value depends on, and then apply a second time to fully converge."
|
||||
errInvalidUnknownDetailSet = "The \"for_each\" set includes values derived from resource attributes that cannot be determined until apply, and so OpenTofu cannot determine the full set of keys that will identify the instances of this resource.\n\nWhen working with unknown values in for_each, it's better to use a map value where the keys are defined statically in your configuration and where only the values contain apply-time results.\n\nAlternatively, you could use the -target planning option to first apply only the resources that the for_each value depends on, and then apply a second time to fully converge."
|
||||
)
|
||||
|
||||
type ContextFunc func(refs []*addrs.Reference) (*hcl.EvalContext, tfdiags.Diagnostics)
|
||||
|
||||
// EvaluateForEachExpression is our standard mechanism for interpreting an
|
||||
// expression given for a "for_each" argument on a resource or a module. This
|
||||
// should be called during expansion in order to determine the final keys and
|
||||
// values.
|
||||
//
|
||||
// evaluateForEachExpression differs from evaluateForEachExpressionValue by
|
||||
// EvaluateForEachExpression differs from EvaluateForEachExpressionValue by
|
||||
// returning an error if the count value is not known, and converting the
|
||||
// cty.Value to a map[string]cty.Value for compatibility with other calls.
|
||||
func evaluateForEachExpression(expr hcl.Expression, ctx EvalContext) (forEach map[string]cty.Value, diags tfdiags.Diagnostics) {
|
||||
func EvaluateForEachExpression(expr hcl.Expression, ctx ContextFunc) (map[string]cty.Value, tfdiags.Diagnostics) {
|
||||
const unknownsNotAllowed = false
|
||||
const tupleNotAllowed = false
|
||||
forEachVal, diags := evaluateForEachExpressionValue(expr, ctx, unknownsNotAllowed, tupleNotAllowed)
|
||||
forEachVal, diags := EvaluateForEachExpressionValue(expr, ctx, unknownsNotAllowed, tupleNotAllowed)
|
||||
// forEachVal might be unknown, but if it is then there should already
|
||||
// be an error about it in diags, which we'll return below.
|
||||
|
||||
@ -40,11 +45,11 @@ func evaluateForEachExpression(expr hcl.Expression, ctx EvalContext) (forEach ma
|
||||
return forEachVal.AsValueMap(), diags
|
||||
}
|
||||
|
||||
// evaluateForEachExpressionValue is like evaluateForEachExpression
|
||||
// EvaluateForEachExpressionValue is like EvaluateForEachExpression
|
||||
// except that it returns a cty.Value map or set which can be unknown.
|
||||
// The 'allowTuple' argument is used to support evaluating for_each from tuple
|
||||
// values, and is currently supported when using for_each in import blocks.
|
||||
func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext, allowUnknown bool, allowTuple bool) (cty.Value, tfdiags.Diagnostics) {
|
||||
func EvaluateForEachExpressionValue(expr hcl.Expression, ctx ContextFunc, allowUnknown bool, allowTuple bool) (cty.Value, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
nullMap := cty.NullVal(cty.Map(cty.DynamicPseudoType))
|
||||
|
||||
@ -54,15 +59,8 @@ func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext, allowU
|
||||
|
||||
refs, moreDiags := lang.ReferencesInExpr(addrs.ParseRef, expr)
|
||||
diags = diags.Append(moreDiags)
|
||||
scope := ctx.EvaluationScope(nil, nil, EvalDataForNoInstanceKey)
|
||||
var hclCtx *hcl.EvalContext
|
||||
if scope != nil {
|
||||
hclCtx, moreDiags = scope.EvalContext(refs)
|
||||
} else {
|
||||
// This shouldn't happen in real code, but it can unfortunately arise
|
||||
// in unit tests due to incompletely-implemented mocks. :(
|
||||
hclCtx = &hcl.EvalContext{}
|
||||
}
|
||||
hclCtx, moreDiags = ctx(refs)
|
||||
diags = diags.Append(moreDiags)
|
||||
if diags.HasErrors() { // Can't continue if we don't even have a valid scope
|
||||
return nullMap, diags
|
||||
@ -81,7 +79,7 @@ func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext, allowU
|
||||
Subject: expr.Range().Ptr(),
|
||||
Expression: expr,
|
||||
EvalContext: hclCtx,
|
||||
Extra: diagnosticCausedBySensitive(true),
|
||||
Extra: DiagnosticCausedBySensitive(true),
|
||||
})
|
||||
}
|
||||
|
||||
@ -114,8 +112,19 @@ func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext, allowU
|
||||
return nullMap, diags
|
||||
}
|
||||
|
||||
const errInvalidUnknownDetailMap = "The \"for_each\" map includes keys derived from resource attributes that cannot be determined until apply, and so OpenTofu cannot determine the full set of keys that will identify the instances of this resource.\n\nWhen working with unknown values in for_each, it's better to define the map keys statically in your configuration and place apply-time results only in the map values.\n\nAlternatively, you could use the -target planning option to first apply only the resources that the for_each value depends on, and then apply a second time to fully converge."
|
||||
const errInvalidUnknownDetailSet = "The \"for_each\" set includes values derived from resource attributes that cannot be determined until apply, and so OpenTofu cannot determine the full set of keys that will identify the instances of this resource.\n\nWhen working with unknown values in for_each, it's better to use a map value where the keys are defined statically in your configuration and where only the values contain apply-time results.\n\nAlternatively, you could use the -target planning option to first apply only the resources that the for_each value depends on, and then apply a second time to fully converge."
|
||||
forEachVal, diags = performForEachValueChecks(expr, hclCtx, allowUnknown, forEachVal, allowedTypesMessage)
|
||||
if diags.HasErrors() {
|
||||
return forEachVal, diags
|
||||
}
|
||||
|
||||
return forEachVal, nil
|
||||
}
|
||||
|
||||
// performForEachValueChecks ensures the for_each argument is valid
|
||||
func performForEachValueChecks(expr hcl.Expression, hclCtx *hcl.EvalContext, allowUnknown bool, forEachVal cty.Value, allowedTypesMessage string) (cty.Value, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
nullMap := cty.NullVal(cty.Map(cty.DynamicPseudoType))
|
||||
ty := forEachVal.Type()
|
||||
|
||||
switch {
|
||||
case forEachVal.IsNull():
|
||||
@ -145,7 +154,7 @@ func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext, allowU
|
||||
Subject: expr.Range().Ptr(),
|
||||
Expression: expr,
|
||||
EvalContext: hclCtx,
|
||||
Extra: diagnosticCausedByUnknown(true),
|
||||
Extra: DiagnosticCausedByUnknown(true),
|
||||
})
|
||||
}
|
||||
// ensure that we have a map, and not a DynamicValue
|
||||
@ -158,55 +167,68 @@ func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext, allowU
|
||||
}
|
||||
|
||||
if ty.IsSetType() {
|
||||
// since we can't use a set values that are unknown, we treat the
|
||||
// entire set as unknown
|
||||
if !forEachVal.IsWhollyKnown() {
|
||||
if !allowUnknown {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid for_each argument",
|
||||
Detail: errInvalidUnknownDetailSet,
|
||||
Subject: expr.Range().Ptr(),
|
||||
Expression: expr,
|
||||
EvalContext: hclCtx,
|
||||
Extra: diagnosticCausedByUnknown(true),
|
||||
})
|
||||
}
|
||||
return cty.UnknownVal(ty), diags
|
||||
setVal, setTypeDiags := performSetTypeChecks(expr, hclCtx, allowUnknown, forEachVal)
|
||||
diags = diags.Append(setTypeDiags)
|
||||
if diags.HasErrors() {
|
||||
return setVal, diags
|
||||
}
|
||||
}
|
||||
|
||||
if ty.ElementType() != cty.String {
|
||||
return forEachVal, diags
|
||||
}
|
||||
|
||||
// performSetTypeChecks does checks when we have a Set type, as sets have some gotchas
|
||||
func performSetTypeChecks(expr hcl.Expression, hclCtx *hcl.EvalContext, allowUnknown bool, forEachVal cty.Value) (cty.Value, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
ty := forEachVal.Type()
|
||||
|
||||
// since we can't use a set values that are unknown, we treat the
|
||||
// entire set as unknown
|
||||
if !forEachVal.IsWhollyKnown() {
|
||||
if !allowUnknown {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid for_each argument",
|
||||
Detail: errInvalidUnknownDetailSet,
|
||||
Subject: expr.Range().Ptr(),
|
||||
Expression: expr,
|
||||
EvalContext: hclCtx,
|
||||
Extra: DiagnosticCausedByUnknown(true),
|
||||
})
|
||||
}
|
||||
return cty.UnknownVal(ty), diags
|
||||
}
|
||||
|
||||
if ty.ElementType() != cty.String {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid for_each set argument",
|
||||
Detail: fmt.Sprintf(`The given "for_each" argument value is unsuitable: "for_each" supports sets of strings, but you have provided a set containing type %s.`, forEachVal.Type().ElementType().FriendlyName()),
|
||||
Subject: expr.Range().Ptr(),
|
||||
Expression: expr,
|
||||
EvalContext: hclCtx,
|
||||
})
|
||||
return cty.NullVal(ty), diags
|
||||
}
|
||||
|
||||
// A set of strings may contain null, which makes it impossible to
|
||||
// convert to a map, so we must return an error
|
||||
it := forEachVal.ElementIterator()
|
||||
for it.Next() {
|
||||
item, _ := it.Element()
|
||||
if item.IsNull() {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid for_each set argument",
|
||||
Detail: fmt.Sprintf(`The given "for_each" argument value is unsuitable: "for_each" supports sets of strings, but you have provided a set containing type %s.`, forEachVal.Type().ElementType().FriendlyName()),
|
||||
Detail: `The given "for_each" argument value is unsuitable: "for_each" sets must not contain null values.`,
|
||||
Subject: expr.Range().Ptr(),
|
||||
Expression: expr,
|
||||
EvalContext: hclCtx,
|
||||
})
|
||||
return cty.NullVal(ty), diags
|
||||
}
|
||||
|
||||
// A set of strings may contain null, which makes it impossible to
|
||||
// convert to a map, so we must return an error
|
||||
it := forEachVal.ElementIterator()
|
||||
for it.Next() {
|
||||
item, _ := it.Element()
|
||||
if item.IsNull() {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid for_each set argument",
|
||||
Detail: `The given "for_each" argument value is unsuitable: "for_each" sets must not contain null values.`,
|
||||
Subject: expr.Range().Ptr(),
|
||||
Expression: expr,
|
||||
EvalContext: hclCtx,
|
||||
})
|
||||
return cty.NullVal(ty), diags
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return forEachVal, nil
|
||||
return forEachVal, diags
|
||||
}
|
||||
|
||||
// markSafeLengthInt allows calling LengthInt on marked values safely
|
@ -2,8 +2,7 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package tofu
|
||||
package evalchecks
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
@ -13,12 +12,22 @@ import (
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hcltest"
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/lang/marks"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func TestEvaluateForEachExpression_valid(t *testing.T) {
|
||||
// returns a mock ref function for the unit tests
|
||||
func mockRefsFunc() ContextFunc {
|
||||
return func(_ []*addrs.Reference) (*hcl.EvalContext, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
evalContext := hcl.EvalContext{}
|
||||
return &evalContext, diags
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateForEachExpression(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
Expr hcl.Expression
|
||||
ForEachMap map[string]cty.Value
|
||||
@ -72,9 +81,7 @@ func TestEvaluateForEachExpression_valid(t *testing.T) {
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx := &MockEvalContext{}
|
||||
ctx.installSimpleEval()
|
||||
forEachMap, diags := evaluateForEachExpression(test.Expr, ctx)
|
||||
forEachMap, diags := EvaluateForEachExpression(test.Expr, mockRefsFunc())
|
||||
|
||||
if len(diags) != 0 {
|
||||
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
|
||||
@ -86,7 +93,6 @@ func TestEvaluateForEachExpression_valid(t *testing.T) {
|
||||
spew.Sdump(forEachMap), spew.Sdump(test.ForEachMap),
|
||||
)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -182,9 +188,7 @@ func TestEvaluateForEachExpression_errors(t *testing.T) {
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx := &MockEvalContext{}
|
||||
ctx.installSimpleEval()
|
||||
_, diags := evaluateForEachExpression(test.Expr, ctx)
|
||||
_, diags := EvaluateForEachExpression(test.Expr, mockRefsFunc())
|
||||
|
||||
if len(diags) != 1 {
|
||||
t.Fatalf("got %d diagnostics; want 1", diags)
|
||||
@ -289,9 +293,7 @@ func TestEvaluateForEachExpression_multi_errors(t *testing.T) {
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx := &MockEvalContext{}
|
||||
ctx.installSimpleEval()
|
||||
_, diags := evaluateForEachExpression(test.Expr, ctx)
|
||||
_, diags := EvaluateForEachExpression(test.Expr, mockRefsFunc())
|
||||
if len(diags) != len(test.Wanted) {
|
||||
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
|
||||
}
|
||||
@ -334,9 +336,7 @@ func TestEvaluateForEachExpressionKnown(t *testing.T) {
|
||||
|
||||
for name, expr := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx := &MockEvalContext{}
|
||||
ctx.installSimpleEval()
|
||||
forEachVal, diags := evaluateForEachExpressionValue(expr, ctx, true, true)
|
||||
forEachVal, diags := EvaluateForEachExpressionValue(expr, mockRefsFunc(), true, true)
|
||||
|
||||
if len(diags) != 0 {
|
||||
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
|
||||
@ -382,9 +382,7 @@ func TestEvaluateForEachExpressionValueTuple(t *testing.T) {
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx := &MockEvalContext{}
|
||||
ctx.installSimpleEval()
|
||||
_, diags := evaluateForEachExpressionValue(test.Expr, ctx, true, test.AllowTuple)
|
||||
_, diags := EvaluateForEachExpressionValue(test.Expr, mockRefsFunc(), true, test.AllowTuple)
|
||||
|
||||
if test.ExpectedError == "" {
|
||||
if len(diags) != 0 {
|
||||
@ -395,7 +393,6 @@ func TestEvaluateForEachExpressionValueTuple(t *testing.T) {
|
||||
t.Errorf("wrong diagnostic detail\ngot: %s\nwant substring: %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package tofu
|
||||
|
||||
import (
|
||||
@ -12,6 +11,7 @@ import (
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/instances"
|
||||
"github.com/opentofu/opentofu/internal/lang/evalchecks"
|
||||
"github.com/opentofu/opentofu/internal/lang/marks"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
@ -51,7 +51,7 @@ func evaluateImportIdExpression(expr hcl.Expression, ctx EvalContext, keyData in
|
||||
Subject: expr.Range().Ptr(),
|
||||
// Expression:
|
||||
// EvalContext:
|
||||
Extra: diagnosticCausedByUnknown(true),
|
||||
Extra: evalchecks.DiagnosticCausedByUnknown(true),
|
||||
})
|
||||
}
|
||||
|
||||
|
49
internal/tofu/eval_iteration.go
Normal file
49
internal/tofu/eval_iteration.go
Normal file
@ -0,0 +1,49 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package tofu
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/lang/evalchecks"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
)
|
||||
|
||||
func evalContextScope(ctx EvalContext) evalchecks.ContextFunc {
|
||||
scope := ctx.EvaluationScope(nil, nil, EvalDataForNoInstanceKey)
|
||||
return func(refs []*addrs.Reference) (*hcl.EvalContext, tfdiags.Diagnostics) {
|
||||
if scope == nil {
|
||||
// This shouldn't happen in real code, but it can unfortunately arise
|
||||
// in unit tests due to incompletely-implemented mocks. :(
|
||||
return &hcl.EvalContext{}, nil
|
||||
}
|
||||
return scope.EvalContext(refs)
|
||||
}
|
||||
}
|
||||
|
||||
func evalContextEvaluate(ctx EvalContext) evalchecks.EvaluateFunc {
|
||||
return func(expr hcl.Expression) (cty.Value, tfdiags.Diagnostics) {
|
||||
return ctx.EvaluateExpr(expr, cty.Number, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func evaluateForEachExpression(expr hcl.Expression, ctx EvalContext) (map[string]cty.Value, tfdiags.Diagnostics) {
|
||||
return evalchecks.EvaluateForEachExpression(expr, evalContextScope(ctx))
|
||||
}
|
||||
|
||||
func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext, allowUnknown bool, allowTuple bool) (cty.Value, tfdiags.Diagnostics) {
|
||||
return evalchecks.EvaluateForEachExpressionValue(expr, evalContextScope(ctx), allowUnknown, allowTuple)
|
||||
}
|
||||
|
||||
func evaluateCountExpression(expr hcl.Expression, ctx EvalContext) (int, tfdiags.Diagnostics) {
|
||||
return evalchecks.EvaluateCountExpression(expr, evalContextEvaluate(ctx))
|
||||
}
|
||||
|
||||
func evaluateCountExpressionValue(expr hcl.Expression, ctx EvalContext) (cty.Value, tfdiags.Diagnostics) {
|
||||
return evalchecks.EvaluateCountExpressionValue(expr, evalContextEvaluate(ctx))
|
||||
}
|
@ -34,8 +34,10 @@ func TestBuildProviderConfig(t *testing.T) {
|
||||
},
|
||||
}
|
||||
gotBody := buildProviderConfig(ctx, providerAddr, &configs.Provider{
|
||||
Name: "foo",
|
||||
Config: configBody,
|
||||
ProviderCommon: configs.ProviderCommon{
|
||||
Name: "foo",
|
||||
Config: configBody,
|
||||
},
|
||||
})
|
||||
|
||||
schema := &configschema.Block{
|
||||
|
@ -78,7 +78,12 @@ func (n *NodeApplyableProvider) ValidateProvider(ctx EvalContext, provider provi
|
||||
configSchema = &configschema.Block{}
|
||||
}
|
||||
|
||||
configVal, _, evalDiags := ctx.EvaluateBlock(configBody, configSchema, nil, EvalDataForNoInstanceKey)
|
||||
data := EvalDataForNoInstanceKey
|
||||
if n.Config != nil {
|
||||
data = n.Config.InstanceData
|
||||
}
|
||||
|
||||
configVal, _, evalDiags := ctx.EvaluateBlock(configBody, configSchema, nil, data)
|
||||
if evalDiags.HasErrors() {
|
||||
return diags.Append(evalDiags)
|
||||
}
|
||||
@ -113,7 +118,12 @@ func (n *NodeApplyableProvider) ConfigureProvider(ctx EvalContext, provider prov
|
||||
}
|
||||
|
||||
configSchema := resp.Provider.Block
|
||||
configVal, configBody, evalDiags := ctx.EvaluateBlock(configBody, configSchema, nil, EvalDataForNoInstanceKey)
|
||||
data := EvalDataForNoInstanceKey
|
||||
if n.Config != nil {
|
||||
data = n.Config.InstanceData
|
||||
}
|
||||
|
||||
configVal, configBody, evalDiags := ctx.EvaluateBlock(configBody, configSchema, nil, data)
|
||||
diags = diags.Append(evalDiags)
|
||||
if evalDiags.HasErrors() {
|
||||
return diags
|
||||
|
@ -22,10 +22,12 @@ import (
|
||||
|
||||
func TestNodeApplyableProviderExecute(t *testing.T) {
|
||||
config := &configs.Provider{
|
||||
Name: "foo",
|
||||
Config: configs.SynthBody("", map[string]cty.Value{
|
||||
"user": cty.StringVal("hello"),
|
||||
}),
|
||||
ProviderCommon: configs.ProviderCommon{
|
||||
Name: "foo",
|
||||
Config: configs.SynthBody("", map[string]cty.Value{
|
||||
"user": cty.StringVal("hello"),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
schema := &configschema.Block{
|
||||
@ -83,10 +85,12 @@ func TestNodeApplyableProviderExecute(t *testing.T) {
|
||||
|
||||
func TestNodeApplyableProviderExecute_unknownImport(t *testing.T) {
|
||||
config := &configs.Provider{
|
||||
Name: "foo",
|
||||
Config: configs.SynthBody("", map[string]cty.Value{
|
||||
"test_string": cty.UnknownVal(cty.String),
|
||||
}),
|
||||
ProviderCommon: configs.ProviderCommon{
|
||||
Name: "foo",
|
||||
Config: configs.SynthBody("", map[string]cty.Value{
|
||||
"test_string": cty.UnknownVal(cty.String),
|
||||
}),
|
||||
},
|
||||
}
|
||||
provider := mockProviderWithConfigSchema(simpleTestSchema())
|
||||
providerAddr := addrs.AbsProviderConfig{
|
||||
@ -118,10 +122,12 @@ func TestNodeApplyableProviderExecute_unknownImport(t *testing.T) {
|
||||
|
||||
func TestNodeApplyableProviderExecute_unknownApply(t *testing.T) {
|
||||
config := &configs.Provider{
|
||||
Name: "foo",
|
||||
Config: configs.SynthBody("", map[string]cty.Value{
|
||||
"test_string": cty.UnknownVal(cty.String),
|
||||
}),
|
||||
ProviderCommon: configs.ProviderCommon{
|
||||
Name: "foo",
|
||||
Config: configs.SynthBody("", map[string]cty.Value{
|
||||
"test_string": cty.UnknownVal(cty.String),
|
||||
}),
|
||||
},
|
||||
}
|
||||
provider := mockProviderWithConfigSchema(simpleTestSchema())
|
||||
providerAddr := addrs.AbsProviderConfig{
|
||||
@ -154,10 +160,12 @@ func TestNodeApplyableProviderExecute_unknownApply(t *testing.T) {
|
||||
|
||||
func TestNodeApplyableProviderExecute_sensitive(t *testing.T) {
|
||||
config := &configs.Provider{
|
||||
Name: "foo",
|
||||
Config: configs.SynthBody("", map[string]cty.Value{
|
||||
"test_string": cty.StringVal("hello").Mark(marks.Sensitive),
|
||||
}),
|
||||
ProviderCommon: configs.ProviderCommon{
|
||||
Name: "foo",
|
||||
Config: configs.SynthBody("", map[string]cty.Value{
|
||||
"test_string": cty.StringVal("hello").Mark(marks.Sensitive),
|
||||
}),
|
||||
},
|
||||
}
|
||||
provider := mockProviderWithConfigSchema(simpleTestSchema())
|
||||
providerAddr := addrs.AbsProviderConfig{
|
||||
@ -191,10 +199,12 @@ func TestNodeApplyableProviderExecute_sensitive(t *testing.T) {
|
||||
|
||||
func TestNodeApplyableProviderExecute_sensitiveValidate(t *testing.T) {
|
||||
config := &configs.Provider{
|
||||
Name: "foo",
|
||||
Config: configs.SynthBody("", map[string]cty.Value{
|
||||
"test_string": cty.StringVal("hello").Mark(marks.Sensitive),
|
||||
}),
|
||||
ProviderCommon: configs.ProviderCommon{
|
||||
Name: "foo",
|
||||
Config: configs.SynthBody("", map[string]cty.Value{
|
||||
"test_string": cty.StringVal("hello").Mark(marks.Sensitive),
|
||||
}),
|
||||
},
|
||||
}
|
||||
provider := mockProviderWithConfigSchema(simpleTestSchema())
|
||||
providerAddr := addrs.AbsProviderConfig{
|
||||
@ -228,8 +238,10 @@ func TestNodeApplyableProviderExecute_sensitiveValidate(t *testing.T) {
|
||||
|
||||
func TestNodeApplyableProviderExecute_emptyValidate(t *testing.T) {
|
||||
config := &configs.Provider{
|
||||
Name: "foo",
|
||||
Config: configs.SynthBody("", map[string]cty.Value{}),
|
||||
ProviderCommon: configs.ProviderCommon{
|
||||
Name: "foo",
|
||||
Config: configs.SynthBody("", map[string]cty.Value{}),
|
||||
},
|
||||
}
|
||||
provider := mockProviderWithConfigSchema(&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
@ -274,10 +286,12 @@ func TestNodeApplyableProvider_Validate(t *testing.T) {
|
||||
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
config := &configs.Provider{
|
||||
Name: "test",
|
||||
Config: configs.SynthBody("", map[string]cty.Value{
|
||||
"region": cty.StringVal("mars"),
|
||||
}),
|
||||
ProviderCommon: configs.ProviderCommon{
|
||||
Name: "test",
|
||||
Config: configs.SynthBody("", map[string]cty.Value{
|
||||
"region": cty.StringVal("mars"),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
node := NodeApplyableProvider{
|
||||
@ -295,10 +309,12 @@ func TestNodeApplyableProvider_Validate(t *testing.T) {
|
||||
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
config := &configs.Provider{
|
||||
Name: "test",
|
||||
Config: configs.SynthBody("", map[string]cty.Value{
|
||||
"region": cty.MapValEmpty(cty.String),
|
||||
}),
|
||||
ProviderCommon: configs.ProviderCommon{
|
||||
Name: "test",
|
||||
Config: configs.SynthBody("", map[string]cty.Value{
|
||||
"region": cty.MapValEmpty(cty.String),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
node := NodeApplyableProvider{
|
||||
@ -356,10 +372,12 @@ func TestNodeApplyableProvider_ConfigProvider(t *testing.T) {
|
||||
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
config := &configs.Provider{
|
||||
Name: "test",
|
||||
Config: configs.SynthBody("", map[string]cty.Value{
|
||||
"region": cty.StringVal("mars"),
|
||||
}),
|
||||
ProviderCommon: configs.ProviderCommon{
|
||||
Name: "test",
|
||||
Config: configs.SynthBody("", map[string]cty.Value{
|
||||
"region": cty.StringVal("mars"),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
node := NodeApplyableProvider{
|
||||
@ -393,8 +411,10 @@ func TestNodeApplyableProvider_ConfigProvider(t *testing.T) {
|
||||
|
||||
t.Run("missing required config", func(t *testing.T) {
|
||||
config := &configs.Provider{
|
||||
Name: "test",
|
||||
Config: hcl.EmptyBody(),
|
||||
ProviderCommon: configs.ProviderCommon{
|
||||
Name: "test",
|
||||
Config: hcl.EmptyBody(),
|
||||
},
|
||||
}
|
||||
node := NodeApplyableProvider{
|
||||
NodeAbstractProvider: &NodeAbstractProvider{
|
||||
@ -445,10 +465,12 @@ func TestNodeApplyableProvider_ConfigProvider_config_fn_err(t *testing.T) {
|
||||
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
config := &configs.Provider{
|
||||
Name: "test",
|
||||
Config: configs.SynthBody("", map[string]cty.Value{
|
||||
"region": cty.StringVal("mars"),
|
||||
}),
|
||||
ProviderCommon: configs.ProviderCommon{
|
||||
Name: "test",
|
||||
Config: configs.SynthBody("", map[string]cty.Value{
|
||||
"region": cty.StringVal("mars"),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
node := NodeApplyableProvider{
|
||||
@ -482,8 +504,10 @@ func TestNodeApplyableProvider_ConfigProvider_config_fn_err(t *testing.T) {
|
||||
|
||||
t.Run("missing required config", func(t *testing.T) {
|
||||
config := &configs.Provider{
|
||||
Name: "test",
|
||||
Config: hcl.EmptyBody(),
|
||||
ProviderCommon: configs.ProviderCommon{
|
||||
Name: "test",
|
||||
Config: hcl.EmptyBody(),
|
||||
},
|
||||
}
|
||||
node := NodeApplyableProvider{
|
||||
NodeAbstractProvider: &NodeAbstractProvider{
|
||||
|
Loading…
Reference in New Issue
Block a user