2024-02-08 03:48:59 -06:00
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
2023-05-02 10:33:06 -05:00
// SPDX-License-Identifier: MPL-2.0
2023-03-23 03:04:21 -05:00
package addrs
import (
"fmt"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
2023-09-20 06:35:35 -05:00
"github.com/opentofu/opentofu/internal/tfdiags"
2023-03-23 03:04:21 -05:00
)
// Checkable is an interface implemented by all address types that can contain
// condition blocks.
type Checkable interface {
UniqueKeyer
checkableSigil ( )
// CheckRule returns the address of an individual check rule of a specified
// type and index within this checkable container.
CheckRule ( CheckRuleType , int ) CheckRule
// ConfigCheckable returns the address of the configuration construct that
// this Checkable belongs to.
//
// Checkable objects can potentially be dynamically declared during a
// plan operation using constructs like resource for_each, and so
// ConfigCheckable gives us a way to talk about the static containers
// those dynamic objects belong to, in case we wish to group together
// dynamic checkable objects into their static checkable for reporting
// purposes.
ConfigCheckable ( ) ConfigCheckable
CheckableKind ( ) CheckableKind
String ( ) string
}
var (
_ Checkable = AbsResourceInstance { }
_ Checkable = AbsOutputValue { }
)
// CheckableKind describes the different kinds of checkable objects.
type CheckableKind rune
//go:generate go run golang.org/x/tools/cmd/stringer -type=CheckableKind checkable.go
const (
2023-07-10 05:33:45 -05:00
CheckableKindInvalid CheckableKind = 0
CheckableResource CheckableKind = 'R'
CheckableOutputValue CheckableKind = 'O'
CheckableCheck CheckableKind = 'C'
CheckableInputVariable CheckableKind = 'I'
2023-03-23 03:04:21 -05:00
)
// ConfigCheckable is an interfaces implemented by address types that represent
// configuration constructs that can have Checkable addresses associated with
// them.
//
// This address type therefore in a sense represents a container for zero or
// more checkable objects all declared by the same configuration construct,
// so that we can talk about these groups of checkable objects before we're
// ready to decide how many checkable objects belong to each one.
type ConfigCheckable interface {
UniqueKeyer
configCheckableSigil ( )
CheckableKind ( ) CheckableKind
String ( ) string
}
var (
_ ConfigCheckable = ConfigResource { }
_ ConfigCheckable = ConfigOutputValue { }
)
// ParseCheckableStr attempts to parse the given string as a Checkable address
// of the given kind.
//
// This should be the opposite of Checkable.String for any Checkable address
// type, as long as "kind" is set to the value returned by the address's
// CheckableKind method.
//
// We do not typically expect users to write out checkable addresses as input,
// but we use them as part of some of our wire formats for persisting check
// results between runs.
func ParseCheckableStr ( kind CheckableKind , src string ) ( Checkable , tfdiags . Diagnostics ) {
var diags tfdiags . Diagnostics
traversal , parseDiags := hclsyntax . ParseTraversalAbs ( [ ] byte ( src ) , "" , hcl . InitialPos )
diags = diags . Append ( parseDiags )
if parseDiags . HasErrors ( ) {
return nil , diags
}
path , remain , diags := parseModuleInstancePrefix ( traversal )
if diags . HasErrors ( ) {
return nil , diags
}
if remain . IsRelative ( ) {
// (relative means that there's either nothing left or what's next isn't an identifier)
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid checkable address" ,
Detail : "Module path must be followed by either a resource instance address or an output value address." ,
Subject : remain . SourceRange ( ) . Ptr ( ) ,
} )
return nil , diags
}
getCheckableName := func ( keyword string , descriptor string ) ( string , tfdiags . Diagnostics ) {
var diags tfdiags . Diagnostics
var name string
if len ( remain ) != 2 {
diags = diags . Append ( hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid checkable address" ,
Detail : fmt . Sprintf ( "%s address must have only one attribute part after the keyword '%s', giving the name of the %s." , cases . Title ( language . English , cases . NoLower ) . String ( keyword ) , keyword , descriptor ) ,
Subject : remain . SourceRange ( ) . Ptr ( ) ,
} )
}
if remain . RootName ( ) != keyword {
diags = diags . Append ( hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid checkable address" ,
Detail : fmt . Sprintf ( "%s address must follow the module address with the keyword '%s'." , cases . Title ( language . English , cases . NoLower ) . String ( keyword ) , keyword ) ,
Subject : remain . SourceRange ( ) . Ptr ( ) ,
} )
}
if step , ok := remain [ 1 ] . ( hcl . TraverseAttr ) ; ! ok {
diags = diags . Append ( hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid checkable address" ,
Detail : fmt . Sprintf ( "%s address must have only one attribute part after the keyword '%s', giving the name of the %s." , cases . Title ( language . English , cases . NoLower ) . String ( keyword ) , keyword , descriptor ) ,
Subject : remain . SourceRange ( ) . Ptr ( ) ,
} )
} else {
name = step . Name
}
return name , diags
}
// We use "kind" to disambiguate here because unfortunately we've
// historically never reserved "output" as a possible resource type name
// and so it is in principle possible -- albeit unlikely -- that there
// might be a resource whose type is literally "output".
switch kind {
case CheckableResource :
riAddr , moreDiags := parseResourceInstanceUnderModule ( path , remain )
diags = diags . Append ( moreDiags )
if diags . HasErrors ( ) {
return nil , diags
}
return riAddr , diags
case CheckableOutputValue :
name , nameDiags := getCheckableName ( "output" , "output value" )
diags = diags . Append ( nameDiags )
if diags . HasErrors ( ) {
return nil , diags
}
return OutputValue { Name : name } . Absolute ( path ) , diags
case CheckableCheck :
name , nameDiags := getCheckableName ( "check" , "check block" )
diags = diags . Append ( nameDiags )
if diags . HasErrors ( ) {
return nil , diags
}
return Check { Name : name } . Absolute ( path ) , diags
2023-07-10 05:33:45 -05:00
case CheckableInputVariable :
name , nameDiags := getCheckableName ( "var" , "variable value" )
diags = diags . Append ( nameDiags )
if diags . HasErrors ( ) {
return nil , diags
}
return InputVariable { Name : name } . Absolute ( path ) , diags
2023-03-23 03:04:21 -05:00
default :
panic ( fmt . Sprintf ( "unsupported CheckableKind %s" , kind ) )
}
}