blob: 97118a9b237c459d828d2cad3e31186906597a15 [file] [log] [blame]
package configs
import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/tfdiags"
)
var (
// When this attribute is set to plan, the values specified in the override
// block will be used for computed attributes even when planning. It defaults
// to apply, meaning that the values will only be used during apply.
overrideDuringCommand = "override_during"
)
func decodeMockProviderBlock(block *hcl.Block) (*Provider, hcl.Diagnostics) {
var diags hcl.Diagnostics
content, config, moreDiags := block.Body.PartialContent(mockProviderSchema)
diags = append(diags, moreDiags...)
name := block.Labels[0]
nameDiags := checkProviderNameNormalized(name, block.DefRange)
diags = append(diags, nameDiags...)
if nameDiags.HasErrors() {
// If the name is invalid then we mustn't produce a result because
// downstream could try to use it as a provider type and then crash.
return nil, diags
}
provider := &Provider{
Name: name,
NameRange: block.LabelRanges[0],
DeclRange: block.DefRange,
// Mock providers shouldn't need any additional data.
Config: hcl.EmptyBody(),
// Mark this provider as being mocked.
Mock: true,
}
if attr, exists := content.Attributes["alias"]; exists {
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &provider.Alias)
diags = append(diags, valDiags...)
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),
})
}
}
useForPlan, useForPlanDiags := useForPlan(content, false)
diags = append(diags, useForPlanDiags...)
provider.MockDataDuringPlan = useForPlan
var dataDiags hcl.Diagnostics
provider.MockData, dataDiags = decodeMockDataBody(config, useForPlan, MockProviderOverrideSource)
diags = append(diags, dataDiags...)
if attr, exists := content.Attributes["source"]; exists {
sourceDiags := gohcl.DecodeExpression(attr.Expr, nil, &provider.MockDataExternalSource)
diags = append(diags, sourceDiags...)
}
return provider, diags
}
func useForPlan(content *hcl.BodyContent, def bool) (bool, hcl.Diagnostics) {
var diags hcl.Diagnostics
if attr, exists := content.Attributes[overrideDuringCommand]; exists {
switch hcl.ExprAsKeyword(attr.Expr) {
case "plan":
return true, diags
case "apply":
return false, diags
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Invalid %s value", overrideDuringCommand),
Detail: fmt.Sprintf("The %s attribute must be a value of plan or apply.", overrideDuringCommand),
Subject: attr.Range.Ptr(),
})
return def, diags
}
}
return def, diags
}
// MockData packages up all the available mock and override data available to
// a mocked provider.
type MockData struct {
MockResources map[string]*MockResource
MockDataSources map[string]*MockResource
Overrides addrs.Map[addrs.Targetable, *Override]
}
// Merge will merge the target MockData object into the current MockData.
//
// If skipCollisions is true, then Merge will simply ignore any entries within
// other that clash with entries already in data. If skipCollisions is false,
// then we will create diagnostics for each duplicate resource.
func (data *MockData) Merge(other *MockData, skipCollisions bool) (diags hcl.Diagnostics) {
if other == nil {
return diags
}
for name, resource := range other.MockResources {
current, exists := data.MockResources[name]
if !exists {
data.MockResources[name] = resource
continue
}
if skipCollisions {
continue
}
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Duplicate mock resource block",
Detail: fmt.Sprintf("A mock_resource %q block already exists at %s.", name, current.Range),
Subject: resource.TypeRange.Ptr(),
})
}
for name, datasource := range other.MockDataSources {
current, exists := data.MockDataSources[name]
if !exists {
data.MockDataSources[name] = datasource
continue
}
if skipCollisions {
continue
}
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Duplicate mock resource block",
Detail: fmt.Sprintf("A mock_data %q block already exists at %s.", name, current.Range),
Subject: datasource.TypeRange.Ptr(),
})
}
for _, elem := range other.Overrides.Elems {
target, override := elem.Key, elem.Value
current, exists := data.Overrides.GetOk(target)
if !exists {
data.Overrides.Put(target, override)
continue
}
if skipCollisions {
continue
}
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Duplicate override block",
Detail: fmt.Sprintf("An override block for %s already exists at %s.", target, current.Range),
Subject: override.Range.Ptr(),
})
}
return diags
}
// MockResource maps a resource or data source type and name to a set of values
// for that resource.
type MockResource struct {
Mode addrs.ResourceMode
Type string
Defaults cty.Value
// UseForPlan is true if the values should be computed during the planning
// phase.
UseForPlan bool
Range hcl.Range
TypeRange hcl.Range
DefaultsRange hcl.Range
}
type OverrideSource int
const (
UnknownOverrideSource OverrideSource = iota
RunBlockOverrideSource
TestFileOverrideSource
MockProviderOverrideSource
MockDataFileOverrideSource
)
// Override targets a specific module, resource or data source with a set of
// replacement values that should be used in place of whatever the underlying
// provider would normally do.
type Override struct {
Target *addrs.Target
Values cty.Value
// UseForPlan is true if the values should be computed during the planning
// phase.
UseForPlan bool
// Source tells us where this Override was defined.
Source OverrideSource
Range hcl.Range
TypeRange hcl.Range
TargetRange hcl.Range
ValuesRange hcl.Range
}
func decodeMockDataBody(body hcl.Body, useForPlanDefault bool, source OverrideSource) (*MockData, hcl.Diagnostics) {
var diags hcl.Diagnostics
content, contentDiags := body.Content(mockDataSchema)
diags = append(diags, contentDiags...)
data := &MockData{
MockResources: make(map[string]*MockResource),
MockDataSources: make(map[string]*MockResource),
Overrides: addrs.MakeMap[addrs.Targetable, *Override](),
}
for _, block := range content.Blocks {
switch block.Type {
case "mock_resource", "mock_data":
resource, resourceDiags := decodeMockResourceBlock(block, useForPlanDefault)
diags = append(diags, resourceDiags...)
if resource != nil {
switch resource.Mode {
case addrs.ManagedResourceMode:
if previous, ok := data.MockResources[resource.Type]; ok {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Duplicate mock_resource block",
Detail: fmt.Sprintf("A mock_resource block for %s has already been defined at %s.", resource.Type, previous.Range),
Subject: resource.TypeRange.Ptr(),
})
continue
}
data.MockResources[resource.Type] = resource
case addrs.DataResourceMode:
if previous, ok := data.MockDataSources[resource.Type]; ok {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Duplicate mock_data block",
Detail: fmt.Sprintf("A mock_data block for %s has already been defined at %s.", resource.Type, previous.Range),
Subject: resource.TypeRange.Ptr(),
})
continue
}
data.MockDataSources[resource.Type] = resource
}
}
case "override_resource":
override, overrideDiags := decodeOverrideResourceBlock(block, useForPlanDefault, source)
diags = append(diags, overrideDiags...)
if override != nil && override.Target != nil {
subject := override.Target.Subject
if previous, ok := data.Overrides.GetOk(subject); ok {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Duplicate override_resource block",
Detail: fmt.Sprintf("An override_resource block targeting %s has already been defined at %s.", subject, previous.Range),
Subject: override.Range.Ptr(),
})
continue
}
data.Overrides.Put(subject, override)
}
case "override_data":
override, overrideDiags := decodeOverrideDataBlock(block, useForPlanDefault, source)
diags = append(diags, overrideDiags...)
if override != nil && override.Target != nil {
subject := override.Target.Subject
if previous, ok := data.Overrides.GetOk(subject); ok {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Duplicate override_data block",
Detail: fmt.Sprintf("An override_data block targeting %s has already been defined at %s.", subject, previous.Range),
Subject: override.Range.Ptr(),
})
continue
}
data.Overrides.Put(subject, override)
}
}
}
return data, diags
}
func decodeMockResourceBlock(block *hcl.Block, useForPlanDefault bool) (*MockResource, hcl.Diagnostics) {
var diags hcl.Diagnostics
content, contentDiags := block.Body.Content(mockResourceSchema)
diags = append(diags, contentDiags...)
resource := &MockResource{
Type: block.Labels[0],
Range: block.DefRange,
TypeRange: block.LabelRanges[0],
}
switch block.Type {
case "mock_resource":
resource.Mode = addrs.ManagedResourceMode
case "mock_data":
resource.Mode = addrs.DataResourceMode
}
if defaults, exists := content.Attributes["defaults"]; exists {
var defaultDiags hcl.Diagnostics
resource.DefaultsRange = defaults.Range
resource.Defaults, defaultDiags = defaults.Expr.Value(nil)
diags = append(diags, defaultDiags...)
} else {
// It's fine if we don't have any defaults, just means we'll generate
// values for everything ourselves.
resource.Defaults = cty.NilVal
}
useForPlan, useForPlanDiags := useForPlan(content, useForPlanDefault)
diags = append(diags, useForPlanDiags...)
resource.UseForPlan = useForPlan
return resource, diags
}
func decodeOverrideModuleBlock(block *hcl.Block, useForPlanDefault bool, source OverrideSource) (*Override, hcl.Diagnostics) {
override, diags := decodeOverrideBlock(block, "outputs", "override_module", useForPlanDefault, source)
if override.Target != nil {
switch override.Target.Subject.AddrType() {
case addrs.ModuleAddrType, addrs.ModuleInstanceAddrType:
// Do nothing, we're good here.
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid override target",
Detail: fmt.Sprintf("You can only target modules from override_module blocks, not %s.", override.Target.Subject),
Subject: override.TargetRange.Ptr(),
})
return nil, diags
}
}
return override, diags
}
func decodeOverrideResourceBlock(block *hcl.Block, useForPlanDefault bool, source OverrideSource) (*Override, hcl.Diagnostics) {
override, diags := decodeOverrideBlock(block, "values", "override_resource", useForPlanDefault, source)
if override.Target != nil {
var mode addrs.ResourceMode
switch override.Target.Subject.AddrType() {
case addrs.AbsResourceInstanceAddrType:
subject := override.Target.Subject.(addrs.AbsResourceInstance)
mode = subject.Resource.Resource.Mode
case addrs.AbsResourceAddrType:
subject := override.Target.Subject.(addrs.AbsResource)
mode = subject.Resource.Mode
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid override target",
Detail: fmt.Sprintf("You can only target resources from override_resource blocks, not %s.", override.Target.Subject),
Subject: override.TargetRange.Ptr(),
})
return nil, diags
}
if mode != addrs.ManagedResourceMode {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid override target",
Detail: fmt.Sprintf("You can only target resources from override_resource blocks, not %s.", override.Target.Subject),
Subject: override.TargetRange.Ptr(),
})
return nil, diags
}
}
return override, diags
}
func decodeOverrideDataBlock(block *hcl.Block, useForPlanDefault bool, source OverrideSource) (*Override, hcl.Diagnostics) {
override, diags := decodeOverrideBlock(block, "values", "override_data", useForPlanDefault, source)
if override.Target != nil {
var mode addrs.ResourceMode
switch override.Target.Subject.AddrType() {
case addrs.AbsResourceInstanceAddrType:
subject := override.Target.Subject.(addrs.AbsResourceInstance)
mode = subject.Resource.Resource.Mode
case addrs.AbsResourceAddrType:
subject := override.Target.Subject.(addrs.AbsResource)
mode = subject.Resource.Mode
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid override target",
Detail: fmt.Sprintf("You can only target data sources from override_data blocks, not %s.", override.Target.Subject),
Subject: override.TargetRange.Ptr(),
})
return nil, diags
}
if mode != addrs.DataResourceMode {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid override target",
Detail: fmt.Sprintf("You can only target data sources from override_data blocks, not %s.", override.Target.Subject),
Subject: override.TargetRange.Ptr(),
})
return nil, diags
}
}
return override, diags
}
func decodeOverrideBlock(block *hcl.Block, attributeName string, blockName string, useForPlanDefault bool, source OverrideSource) (*Override, hcl.Diagnostics) {
var diags hcl.Diagnostics
content, contentDiags := block.Body.Content(&hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{Name: "target"},
{Name: overrideDuringCommand},
{Name: attributeName},
},
})
diags = append(diags, contentDiags...)
override := &Override{
Source: source,
Range: block.DefRange,
TypeRange: block.TypeRange,
}
if target, exists := content.Attributes["target"]; exists {
override.TargetRange = target.Range
traversal, traversalDiags := hcl.AbsTraversalForExpr(target.Expr)
diags = append(diags, traversalDiags...)
if traversal != nil {
var targetDiags tfdiags.Diagnostics
override.Target, targetDiags = addrs.ParseTarget(traversal)
diags = append(diags, targetDiags.ToHCL()...)
}
} else {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Missing target attribute",
Detail: fmt.Sprintf("%s blocks must specify a target address.", blockName),
Subject: override.Range.Ptr(),
})
}
if attribute, exists := content.Attributes[attributeName]; exists {
var valueDiags hcl.Diagnostics
override.ValuesRange = attribute.Range
override.Values, valueDiags = attribute.Expr.Value(nil)
diags = append(diags, valueDiags...)
} else {
// It's fine if we don't have any values, just means we'll generate
// values for everything ourselves. We set this to an empty object so
// it's equivalent to `values = {}` which makes later processing easier.
override.Values = cty.EmptyObjectVal
}
useForPlan, useForPlanDiags := useForPlan(content, useForPlanDefault)
diags = append(diags, useForPlanDiags...)
override.UseForPlan = useForPlan
if !override.Values.Type().IsObjectType() {
var attributePreposition string
switch attributeName {
case "outputs":
attributePreposition = "an"
default:
attributePreposition = "a"
}
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Invalid %s attribute", attributeName),
Detail: fmt.Sprintf("%s blocks must specify %s %s attribute that is an object.", blockName, attributePreposition, attributeName),
Subject: override.ValuesRange.Ptr(),
})
}
return override, diags
}
var mockProviderSchema = &hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: "alias",
},
{
Name: "source",
},
{
Name: overrideDuringCommand,
},
},
}
var mockDataSchema = &hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{
{Type: "mock_resource", LabelNames: []string{"type"}},
{Type: "mock_data", LabelNames: []string{"type"}},
{Type: "override_resource"},
{Type: "override_data"},
},
}
var mockResourceSchema = &hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{Name: "defaults"},
{Name: overrideDuringCommand},
},
}