2018-02-02 05:33:06 +01:00
package configs
import (
2018-02-03 02:22:25 +01:00
"fmt"
2018-04-06 20:07:39 +02:00
2019-09-10 00:58:44 +02:00
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclsyntax"
2018-04-30 19:15:27 +02:00
"github.com/hashicorp/terraform/addrs"
2018-02-02 05:33:06 +01:00
)
2018-04-06 20:07:39 +02:00
// Resource represents a "resource" or "data" block in a module or file.
type Resource struct {
Mode addrs . ResourceMode
2018-02-02 05:33:06 +01:00
Name string
Type string
Config hcl . Body
Count hcl . Expression
ForEach hcl . Expression
2018-02-03 02:22:25 +01:00
ProviderConfigRef * ProviderConfigRef
2018-02-02 05:33:06 +01:00
DependsOn [ ] hcl . Traversal
2018-04-06 20:07:39 +02:00
// Managed is populated only for Mode = addrs.ManagedResourceMode,
// containing the additional fields that apply to managed resources.
// For all other resource modes, this field is nil.
Managed * ManagedResource
DeclRange hcl . Range
TypeRange hcl . Range
}
// ManagedResource represents a "resource" block in a module or file.
type ManagedResource struct {
2018-02-02 05:33:06 +01:00
Connection * Connection
Provisioners [ ] * Provisioner
CreateBeforeDestroy bool
PreventDestroy bool
IgnoreChanges [ ] hcl . Traversal
2018-02-15 20:25:06 +01:00
IgnoreAllChanges bool
2018-02-02 05:33:06 +01:00
2018-02-07 03:05:14 +01:00
CreateBeforeDestroySet bool
PreventDestroySet bool
2018-02-03 02:22:25 +01:00
}
2018-04-06 20:07:39 +02:00
func ( r * Resource ) moduleUniqueKey ( ) string {
2018-05-03 02:51:01 +02:00
return r . Addr ( ) . String ( )
2018-02-07 01:28:40 +01:00
}
2018-04-30 19:15:27 +02:00
// Addr returns a resource address for the receiver that is relative to the
// resource's containing module.
func ( r * Resource ) Addr ( ) addrs . Resource {
return addrs . Resource {
Mode : r . Mode ,
Type : r . Type ,
Name : r . Name ,
}
}
// ProviderConfigAddr returns the address for the provider configuration
2018-03-28 02:22:51 +02:00
// that should be used for this resource. This function implements the
// default behavior of extracting the type from the resource type name if
// an explicit "provider" argument was not provided.
2018-04-30 19:15:27 +02:00
func ( r * Resource ) ProviderConfigAddr ( ) addrs . ProviderConfig {
2018-03-28 02:22:51 +02:00
if r . ProviderConfigRef == nil {
2018-04-30 19:15:27 +02:00
return r . Addr ( ) . DefaultProviderConfig ( )
2018-03-28 02:22:51 +02:00
}
2018-04-30 19:15:27 +02:00
return addrs . ProviderConfig {
2020-01-28 14:13:30 +01:00
Type : r . ProviderConfigRef . Name ,
2018-04-30 19:15:27 +02:00
Alias : r . ProviderConfigRef . Alias ,
}
2018-03-28 02:22:51 +02:00
}
2018-04-06 20:07:39 +02:00
func decodeResourceBlock ( block * hcl . Block ) ( * Resource , hcl . Diagnostics ) {
2019-11-12 21:10:29 +01:00
var diags hcl . Diagnostics
2018-04-06 20:07:39 +02:00
r := & Resource {
Mode : addrs . ManagedResourceMode ,
2018-02-03 02:22:25 +01:00
Type : block . Labels [ 0 ] ,
Name : block . Labels [ 1 ] ,
DeclRange : block . DefRange ,
TypeRange : block . LabelRanges [ 0 ] ,
2018-04-06 20:07:39 +02:00
Managed : & ManagedResource { } ,
2018-02-03 02:22:25 +01:00
}
2019-11-12 21:10:29 +01:00
// Produce deprecation messages for any pre-0.12-style
// single-interpolation-only expressions. We do this up front here because
// then we can also catch instances inside special blocks like "connection",
// before PartialContent extracts them.
moreDiags := warnForDeprecatedInterpolationsInBody ( block . Body )
diags = append ( diags , moreDiags ... )
content , remain , moreDiags := block . Body . PartialContent ( resourceBlockSchema )
diags = append ( diags , moreDiags ... )
2018-02-03 02:22:25 +01:00
r . Config = remain
if ! hclsyntax . ValidIdentifier ( r . Type ) {
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid resource type name" ,
Detail : badIdentifierDetail ,
Subject : & block . LabelRanges [ 0 ] ,
} )
}
if ! hclsyntax . ValidIdentifier ( r . Name ) {
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid resource name" ,
Detail : badIdentifierDetail ,
2018-03-01 21:18:07 +01:00
Subject : & block . LabelRanges [ 1 ] ,
2018-02-03 02:22:25 +01:00
} )
}
if attr , exists := content . Attributes [ "count" ] ; exists {
r . Count = attr . Expr
}
if attr , exists := content . Attributes [ "for_each" ] ; exists {
2018-11-20 20:53:45 +01:00
r . ForEach = attr . Expr
2019-06-12 17:07:32 +02:00
// Cannot have count and for_each on the same resource block
if r . Count != nil {
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : ` Invalid combination of "count" and "for_each" ` ,
Detail : ` The "count" and "for_each" meta-arguments are mutually-exclusive, only one should be used to be explicit about the number of resources to be created. ` ,
Subject : & attr . NameRange ,
} )
}
2018-02-03 02:22:25 +01:00
}
if attr , exists := content . Attributes [ "provider" ] ; exists {
var providerDiags hcl . Diagnostics
2018-04-24 19:06:51 +02:00
r . ProviderConfigRef , providerDiags = decodeProviderConfigRef ( attr . Expr , "provider" )
2018-02-03 02:22:25 +01:00
diags = append ( diags , providerDiags ... )
}
if attr , exists := content . Attributes [ "depends_on" ] ; exists {
deps , depsDiags := decodeDependsOn ( attr )
diags = append ( diags , depsDiags ... )
r . DependsOn = append ( r . DependsOn , deps ... )
}
var seenLifecycle * hcl . Block
var seenConnection * hcl . Block
for _ , block := range content . Blocks {
switch block . Type {
case "lifecycle" :
if seenLifecycle != nil {
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Duplicate lifecycle block" ,
Detail : fmt . Sprintf ( "This resource already has a lifecycle block at %s." , seenLifecycle . DefRange ) ,
Subject : & block . DefRange ,
} )
continue
}
seenLifecycle = block
lcContent , lcDiags := block . Body . Content ( resourceLifecycleBlockSchema )
diags = append ( diags , lcDiags ... )
if attr , exists := lcContent . Attributes [ "create_before_destroy" ] ; exists {
2018-04-06 20:07:39 +02:00
valDiags := gohcl . DecodeExpression ( attr . Expr , nil , & r . Managed . CreateBeforeDestroy )
2018-02-03 02:22:25 +01:00
diags = append ( diags , valDiags ... )
2018-04-06 20:07:39 +02:00
r . Managed . CreateBeforeDestroySet = true
2018-02-03 02:22:25 +01:00
}
if attr , exists := lcContent . Attributes [ "prevent_destroy" ] ; exists {
2018-04-06 20:07:39 +02:00
valDiags := gohcl . DecodeExpression ( attr . Expr , nil , & r . Managed . PreventDestroy )
2018-02-03 02:22:25 +01:00
diags = append ( diags , valDiags ... )
2018-04-06 20:07:39 +02:00
r . Managed . PreventDestroySet = true
2018-02-03 02:22:25 +01:00
}
if attr , exists := lcContent . Attributes [ "ignore_changes" ] ; exists {
2018-02-15 20:25:06 +01:00
// ignore_changes can either be a list of relative traversals
// or it can be just the keyword "all" to ignore changes to this
// resource entirely.
// ignore_changes = [ami, instance_type]
// ignore_changes = all
// We also allow two legacy forms for compatibility with earlier
// versions:
// ignore_changes = ["ami", "instance_type"]
// ignore_changes = ["*"]
kw := hcl . ExprAsKeyword ( attr . Expr )
switch {
case kw == "all" :
2018-04-06 20:07:39 +02:00
r . Managed . IgnoreAllChanges = true
2018-02-15 20:25:06 +01:00
default :
exprs , listDiags := hcl . ExprList ( attr . Expr )
diags = append ( diags , listDiags ... )
var ignoreAllRange hcl . Range
for _ , expr := range exprs {
// our expr might be the literal string "*", which
// we accept as a deprecated way of saying "all".
if shimIsIgnoreChangesStar ( expr ) {
2018-04-06 20:07:39 +02:00
r . Managed . IgnoreAllChanges = true
2018-02-15 20:25:06 +01:00
ignoreAllRange = expr . Range ( )
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagWarning ,
Summary : "Deprecated ignore_changes wildcard" ,
2018-05-05 04:24:38 +02:00
Detail : "The [\"*\"] form of ignore_changes wildcard is deprecated. Use \"ignore_changes = all\" to ignore changes to all attributes." ,
2018-02-15 20:25:06 +01:00
Subject : attr . Expr . Range ( ) . Ptr ( ) ,
} )
continue
}
expr , shimDiags := shimTraversalInString ( expr , false )
diags = append ( diags , shimDiags ... )
traversal , travDiags := hcl . RelTraversalForExpr ( expr )
diags = append ( diags , travDiags ... )
if len ( traversal ) != 0 {
2018-04-06 20:07:39 +02:00
r . Managed . IgnoreChanges = append ( r . Managed . IgnoreChanges , traversal )
2018-02-15 20:25:06 +01:00
}
}
2018-02-15 19:17:36 +01:00
2018-04-06 20:07:39 +02:00
if r . Managed . IgnoreAllChanges && len ( r . Managed . IgnoreChanges ) != 0 {
2018-02-15 20:25:06 +01:00
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid ignore_changes ruleset" ,
Detail : "Cannot mix wildcard string \"*\" with non-wildcard references." ,
Subject : & ignoreAllRange ,
Context : attr . Expr . Range ( ) . Ptr ( ) ,
} )
2018-02-03 02:22:25 +01:00
}
2018-02-15 20:25:06 +01:00
2018-02-03 02:22:25 +01:00
}
2018-02-15 20:25:06 +01:00
2018-02-03 02:22:25 +01:00
}
case "connection" :
if seenConnection != nil {
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Duplicate connection block" ,
Detail : fmt . Sprintf ( "This resource already has a connection block at %s." , seenConnection . DefRange ) ,
Subject : & block . DefRange ,
} )
continue
}
seenConnection = block
2018-05-29 20:58:28 +02:00
r . Managed . Connection = & Connection {
Config : block . Body ,
DeclRange : block . DefRange ,
}
2018-02-03 02:22:25 +01:00
case "provisioner" :
pv , pvDiags := decodeProvisionerBlock ( block )
diags = append ( diags , pvDiags ... )
if pv != nil {
2018-04-06 20:07:39 +02:00
r . Managed . Provisioners = append ( r . Managed . Provisioners , pv )
2018-02-03 02:22:25 +01:00
}
default :
2018-11-20 20:53:45 +01:00
// Any other block types are ones we've reserved for future use,
// so they get a generic message.
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Reserved block type name in resource block" ,
Detail : fmt . Sprintf ( "The block type name %q is reserved for use by Terraform in a future version." , block . Type ) ,
Subject : & block . TypeRange ,
} )
2018-02-03 02:22:25 +01:00
}
}
2019-12-03 19:23:07 +01:00
// Now we can validate the connection block references if there are any destroy provisioners.
// TODO: should we eliminate standalone connection blocks?
if r . Managed . Connection != nil {
for _ , p := range r . Managed . Provisioners {
if p . When == ProvisionerWhenDestroy {
diags = append ( diags , onlySelfRefs ( r . Managed . Connection . Config ) ... )
break
}
}
}
2018-02-03 02:22:25 +01:00
return r , diags
2018-02-02 05:33:06 +01:00
}
2018-04-06 20:07:39 +02:00
func decodeDataBlock ( block * hcl . Block ) ( * Resource , hcl . Diagnostics ) {
r := & Resource {
Mode : addrs . DataResourceMode ,
2018-02-03 02:22:25 +01:00
Type : block . Labels [ 0 ] ,
Name : block . Labels [ 1 ] ,
DeclRange : block . DefRange ,
TypeRange : block . LabelRanges [ 0 ] ,
}
2018-02-02 05:33:06 +01:00
2018-02-03 02:22:25 +01:00
content , remain , diags := block . Body . PartialContent ( dataBlockSchema )
r . Config = remain
2018-02-02 05:33:06 +01:00
2018-02-03 02:22:25 +01:00
if ! hclsyntax . ValidIdentifier ( r . Type ) {
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid data source name" ,
Detail : badIdentifierDetail ,
Subject : & block . LabelRanges [ 0 ] ,
} )
}
if ! hclsyntax . ValidIdentifier ( r . Name ) {
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid data resource name" ,
Detail : badIdentifierDetail ,
2018-03-01 21:18:07 +01:00
Subject : & block . LabelRanges [ 1 ] ,
2018-02-03 02:22:25 +01:00
} )
}
2018-02-02 05:33:06 +01:00
2018-02-03 02:22:25 +01:00
if attr , exists := content . Attributes [ "count" ] ; exists {
r . Count = attr . Expr
}
if attr , exists := content . Attributes [ "for_each" ] ; exists {
2018-11-20 20:53:45 +01:00
r . ForEach = attr . Expr
2019-07-25 17:51:55 +02:00
// Cannot have count and for_each on the same data block
if r . Count != nil {
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : ` Invalid combination of "count" and "for_each" ` ,
Detail : ` The "count" and "for_each" meta-arguments are mutually-exclusive, only one should be used to be explicit about the number of resources to be created. ` ,
Subject : & attr . NameRange ,
} )
}
2018-02-03 02:22:25 +01:00
}
if attr , exists := content . Attributes [ "provider" ] ; exists {
var providerDiags hcl . Diagnostics
2018-04-24 19:06:51 +02:00
r . ProviderConfigRef , providerDiags = decodeProviderConfigRef ( attr . Expr , "provider" )
2018-02-03 02:22:25 +01:00
diags = append ( diags , providerDiags ... )
}
if attr , exists := content . Attributes [ "depends_on" ] ; exists {
deps , depsDiags := decodeDependsOn ( attr )
diags = append ( diags , depsDiags ... )
r . DependsOn = append ( r . DependsOn , deps ... )
}
for _ , block := range content . Blocks {
2018-11-20 20:53:45 +01:00
// All of the block types we accept are just reserved for future use, but some get a specialized error message.
switch block . Type {
case "lifecycle" :
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Unsupported lifecycle block" ,
Detail : "Data resources do not have lifecycle settings, so a lifecycle block is not allowed." ,
Subject : & block . DefRange ,
} )
default :
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Reserved block type name in data block" ,
Detail : fmt . Sprintf ( "The block type name %q is reserved for use by Terraform in a future version." , block . Type ) ,
Subject : & block . TypeRange ,
} )
}
2018-02-03 02:22:25 +01:00
}
return r , diags
2018-02-02 05:33:06 +01:00
}
2018-02-03 02:22:25 +01:00
type ProviderConfigRef struct {
Name string
NameRange hcl . Range
Alias string
AliasRange * hcl . Range // nil if alias not set
}
2018-02-02 05:33:06 +01:00
2018-04-24 19:06:51 +02:00
func decodeProviderConfigRef ( expr hcl . Expression , argName string ) ( * ProviderConfigRef , hcl . Diagnostics ) {
2018-02-03 02:22:25 +01:00
var diags hcl . Diagnostics
2018-02-15 19:17:36 +01:00
2018-04-24 19:06:51 +02:00
var shimDiags hcl . Diagnostics
expr , shimDiags = shimTraversalInString ( expr , false )
2018-02-15 19:17:36 +01:00
diags = append ( diags , shimDiags ... )
traversal , travDiags := hcl . AbsTraversalForExpr ( expr )
2018-02-02 05:33:06 +01:00
2018-02-03 02:22:25 +01:00
// AbsTraversalForExpr produces only generic errors, so we'll discard
// the errors given and produce our own with extra context. If we didn't
// get any errors then we might still have warnings, though.
if ! travDiags . HasErrors ( ) {
diags = append ( diags , travDiags ... )
}
2018-02-02 05:33:06 +01:00
2018-05-05 04:25:16 +02:00
if len ( traversal ) < 1 || len ( traversal ) > 2 {
2018-02-03 02:22:25 +01:00
// A provider reference was given as a string literal in the legacy
// configuration language and there are lots of examples out there
// showing that usage, so we'll sniff for that situation here and
// produce a specialized error message for it to help users find
// the new correct form.
2018-04-24 19:06:51 +02:00
if exprIsNativeQuotedString ( expr ) {
2018-02-03 02:22:25 +01:00
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid provider configuration reference" ,
Detail : "A provider configuration reference must not be given in quotes." ,
2018-02-15 19:17:36 +01:00
Subject : expr . Range ( ) . Ptr ( ) ,
2018-02-03 02:22:25 +01:00
} )
return nil , diags
}
2018-02-02 05:33:06 +01:00
2018-02-03 02:22:25 +01:00
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid provider configuration reference" ,
2018-04-24 19:06:51 +02:00
Detail : fmt . Sprintf ( "The %s argument requires a provider type name, optionally followed by a period and then a configuration alias." , argName ) ,
2018-02-15 19:17:36 +01:00
Subject : expr . Range ( ) . Ptr ( ) ,
2018-02-03 02:22:25 +01:00
} )
return nil , diags
}
2018-02-02 05:33:06 +01:00
2018-02-03 02:22:25 +01:00
ret := & ProviderConfigRef {
Name : traversal . RootName ( ) ,
NameRange : traversal [ 0 ] . SourceRange ( ) ,
}
if len ( traversal ) > 1 {
aliasStep , ok := traversal [ 1 ] . ( hcl . TraverseAttr )
if ! ok {
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid provider configuration reference" ,
Detail : "Provider name must either stand alone or be followed by a period and then a configuration alias." ,
Subject : traversal [ 1 ] . SourceRange ( ) . Ptr ( ) ,
} )
return ret , diags
}
ret . Alias = aliasStep . Name
ret . AliasRange = aliasStep . SourceRange ( ) . Ptr ( )
}
return ret , diags
}
2018-04-30 19:15:27 +02:00
// Addr returns the provider config address corresponding to the receiving
// config reference.
//
// This is a trivial conversion, essentially just discarding the source
// location information and keeping just the addressing information.
func ( r * ProviderConfigRef ) Addr ( ) addrs . ProviderConfig {
return addrs . ProviderConfig {
2020-01-28 14:13:30 +01:00
Type : r . Name ,
2018-04-30 19:15:27 +02:00
Alias : r . Alias ,
}
}
2018-03-28 02:22:51 +02:00
func ( r * ProviderConfigRef ) String ( ) string {
if r == nil {
return "<nil>"
}
if r . Alias != "" {
return fmt . Sprintf ( "%s.%s" , r . Name , r . Alias )
}
return r . Name
}
2018-02-03 02:22:25 +01:00
var commonResourceAttributes = [ ] hcl . AttributeSchema {
{
Name : "count" ,
} ,
{
Name : "for_each" ,
} ,
{
Name : "provider" ,
} ,
{
Name : "depends_on" ,
} ,
}
var resourceBlockSchema = & hcl . BodySchema {
Attributes : commonResourceAttributes ,
Blocks : [ ] hcl . BlockHeaderSchema {
2018-11-20 20:53:45 +01:00
{ Type : "locals" } , // reserved for future use
{ Type : "lifecycle" } ,
{ Type : "connection" } ,
{ Type : "provisioner" , LabelNames : [ ] string { "type" } } ,
2018-02-03 02:22:25 +01:00
} ,
}
var dataBlockSchema = & hcl . BodySchema {
Attributes : commonResourceAttributes ,
Blocks : [ ] hcl . BlockHeaderSchema {
2018-11-20 20:53:45 +01:00
{ Type : "lifecycle" } , // reserved for future use
{ Type : "locals" } , // reserved for future use
2018-02-03 02:22:25 +01:00
} ,
}
var resourceLifecycleBlockSchema = & hcl . BodySchema {
Attributes : [ ] hcl . AttributeSchema {
{
Name : "create_before_destroy" ,
} ,
{
Name : "prevent_destroy" ,
} ,
{
Name : "ignore_changes" ,
} ,
} ,
}