// 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/opentofu/opentofu/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' CheckableInputVariable CheckableKind = 'I' ) // 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 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 default: panic(fmt.Sprintf("unsupported CheckableKind %s", kind)) } }