github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/addrs/checkable.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package addrs
     5  
     6  import (
     7  	"fmt"
     8  
     9  	"golang.org/x/text/cases"
    10  	"golang.org/x/text/language"
    11  
    12  	"github.com/hashicorp/hcl/v2"
    13  	"github.com/hashicorp/hcl/v2/hclsyntax"
    14  
    15  	"github.com/terramate-io/tf/tfdiags"
    16  )
    17  
    18  // Checkable is an interface implemented by all address types that can contain
    19  // condition blocks.
    20  type Checkable interface {
    21  	UniqueKeyer
    22  
    23  	checkableSigil()
    24  
    25  	// CheckRule returns the address of an individual check rule of a specified
    26  	// type and index within this checkable container.
    27  	CheckRule(CheckRuleType, int) CheckRule
    28  
    29  	// ConfigCheckable returns the address of the configuration construct that
    30  	// this Checkable belongs to.
    31  	//
    32  	// Checkable objects can potentially be dynamically declared during a
    33  	// plan operation using constructs like resource for_each, and so
    34  	// ConfigCheckable gives us a way to talk about the static containers
    35  	// those dynamic objects belong to, in case we wish to group together
    36  	// dynamic checkable objects into their static checkable for reporting
    37  	// purposes.
    38  	ConfigCheckable() ConfigCheckable
    39  
    40  	CheckableKind() CheckableKind
    41  	String() string
    42  }
    43  
    44  var (
    45  	_ Checkable = AbsResourceInstance{}
    46  	_ Checkable = AbsOutputValue{}
    47  )
    48  
    49  // CheckableKind describes the different kinds of checkable objects.
    50  type CheckableKind rune
    51  
    52  //go:generate go run golang.org/x/tools/cmd/stringer -type=CheckableKind checkable.go
    53  
    54  const (
    55  	CheckableKindInvalid   CheckableKind = 0
    56  	CheckableResource      CheckableKind = 'R'
    57  	CheckableOutputValue   CheckableKind = 'O'
    58  	CheckableCheck         CheckableKind = 'C'
    59  	CheckableInputVariable CheckableKind = 'I'
    60  )
    61  
    62  // ConfigCheckable is an interfaces implemented by address types that represent
    63  // configuration constructs that can have Checkable addresses associated with
    64  // them.
    65  //
    66  // This address type therefore in a sense represents a container for zero or
    67  // more checkable objects all declared by the same configuration construct,
    68  // so that we can talk about these groups of checkable objects before we're
    69  // ready to decide how many checkable objects belong to each one.
    70  type ConfigCheckable interface {
    71  	UniqueKeyer
    72  
    73  	configCheckableSigil()
    74  
    75  	CheckableKind() CheckableKind
    76  	String() string
    77  }
    78  
    79  var (
    80  	_ ConfigCheckable = ConfigResource{}
    81  	_ ConfigCheckable = ConfigOutputValue{}
    82  )
    83  
    84  // ParseCheckableStr attempts to parse the given string as a Checkable address
    85  // of the given kind.
    86  //
    87  // This should be the opposite of Checkable.String for any Checkable address
    88  // type, as long as "kind" is set to the value returned by the address's
    89  // CheckableKind method.
    90  //
    91  // We do not typically expect users to write out checkable addresses as input,
    92  // but we use them as part of some of our wire formats for persisting check
    93  // results between runs.
    94  func ParseCheckableStr(kind CheckableKind, src string) (Checkable, tfdiags.Diagnostics) {
    95  	var diags tfdiags.Diagnostics
    96  
    97  	traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(src), "", hcl.InitialPos)
    98  	diags = diags.Append(parseDiags)
    99  	if parseDiags.HasErrors() {
   100  		return nil, diags
   101  	}
   102  
   103  	path, remain, diags := parseModuleInstancePrefix(traversal)
   104  	if diags.HasErrors() {
   105  		return nil, diags
   106  	}
   107  
   108  	if remain.IsRelative() {
   109  		// (relative means that there's either nothing left or what's next isn't an identifier)
   110  		diags = diags.Append(&hcl.Diagnostic{
   111  			Severity: hcl.DiagError,
   112  			Summary:  "Invalid checkable address",
   113  			Detail:   "Module path must be followed by either a resource instance address or an output value address.",
   114  			Subject:  remain.SourceRange().Ptr(),
   115  		})
   116  		return nil, diags
   117  	}
   118  
   119  	getCheckableName := func(keyword string, descriptor string) (string, tfdiags.Diagnostics) {
   120  		var diags tfdiags.Diagnostics
   121  		var name string
   122  
   123  		if len(remain) != 2 {
   124  			diags = diags.Append(hcl.Diagnostic{
   125  				Severity: hcl.DiagError,
   126  				Summary:  "Invalid checkable address",
   127  				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),
   128  				Subject:  remain.SourceRange().Ptr(),
   129  			})
   130  		}
   131  
   132  		if remain.RootName() != keyword {
   133  			diags = diags.Append(hcl.Diagnostic{
   134  				Severity: hcl.DiagError,
   135  				Summary:  "Invalid checkable address",
   136  				Detail:   fmt.Sprintf("%s address must follow the module address with the keyword '%s'.", cases.Title(language.English, cases.NoLower).String(keyword), keyword),
   137  				Subject:  remain.SourceRange().Ptr(),
   138  			})
   139  		}
   140  		if step, ok := remain[1].(hcl.TraverseAttr); !ok {
   141  			diags = diags.Append(hcl.Diagnostic{
   142  				Severity: hcl.DiagError,
   143  				Summary:  "Invalid checkable address",
   144  				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),
   145  				Subject:  remain.SourceRange().Ptr(),
   146  			})
   147  		} else {
   148  			name = step.Name
   149  		}
   150  
   151  		return name, diags
   152  	}
   153  
   154  	// We use "kind" to disambiguate here because unfortunately we've
   155  	// historically never reserved "output" as a possible resource type name
   156  	// and so it is in principle possible -- albeit unlikely -- that there
   157  	// might be a resource whose type is literally "output".
   158  	switch kind {
   159  	case CheckableResource:
   160  		riAddr, moreDiags := parseResourceInstanceUnderModule(path, remain)
   161  		diags = diags.Append(moreDiags)
   162  		if diags.HasErrors() {
   163  			return nil, diags
   164  		}
   165  		return riAddr, diags
   166  
   167  	case CheckableOutputValue:
   168  		name, nameDiags := getCheckableName("output", "output value")
   169  		diags = diags.Append(nameDiags)
   170  		if diags.HasErrors() {
   171  			return nil, diags
   172  		}
   173  		return OutputValue{Name: name}.Absolute(path), diags
   174  
   175  	case CheckableCheck:
   176  		name, nameDiags := getCheckableName("check", "check block")
   177  		diags = diags.Append(nameDiags)
   178  		if diags.HasErrors() {
   179  			return nil, diags
   180  		}
   181  		return Check{Name: name}.Absolute(path), diags
   182  
   183  	case CheckableInputVariable:
   184  		name, nameDiags := getCheckableName("var", "variable value")
   185  		diags = diags.Append(nameDiags)
   186  		if diags.HasErrors() {
   187  			return nil, diags
   188  		}
   189  		return InputVariable{Name: name}.Absolute(path), diags
   190  
   191  	default:
   192  		panic(fmt.Sprintf("unsupported CheckableKind %s", kind))
   193  	}
   194  }