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 }