| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| 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" |
| |
| "github.com/hashicorp/terraform/internal/tfdiags" |
| ) |
| |
| // 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 ( |
| CheckableKindInvalid CheckableKind = 0 |
| CheckableResource CheckableKind = 'R' |
| CheckableOutputValue CheckableKind = 'O' |
| CheckableCheck CheckableKind = 'C' |
| ) |
| |
| // 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 |
| |
| default: |
| panic(fmt.Sprintf("unsupported CheckableKind %s", kind)) |
| } |
| } |