| package addrs |
| |
| import ( |
| "fmt" |
| "strings" |
| |
| "github.com/hashicorp/hcl/v2" |
| "github.com/hashicorp/hcl/v2/hclsyntax" |
| "github.com/hashicorp/terraform/internal/tfdiags" |
| "github.com/zclconf/go-cty/cty" |
| ) |
| |
| // Reference describes a reference to an address with source location |
| // information. |
| type Reference struct { |
| Subject Referenceable |
| SourceRange tfdiags.SourceRange |
| Remaining hcl.Traversal |
| } |
| |
| // DisplayString returns a string that approximates the subject and remaining |
| // traversal of the reciever in a way that resembles the Terraform language |
| // syntax that could've produced it. |
| // |
| // It's not guaranteed to actually be a valid Terraform language expression, |
| // since the intended use here is primarily for UI messages such as |
| // diagnostics. |
| func (r *Reference) DisplayString() string { |
| if len(r.Remaining) == 0 { |
| // Easy case: we can just return the subject's string. |
| return r.Subject.String() |
| } |
| |
| var ret strings.Builder |
| ret.WriteString(r.Subject.String()) |
| for _, step := range r.Remaining { |
| switch tStep := step.(type) { |
| case hcl.TraverseRoot: |
| ret.WriteString(tStep.Name) |
| case hcl.TraverseAttr: |
| ret.WriteByte('.') |
| ret.WriteString(tStep.Name) |
| case hcl.TraverseIndex: |
| ret.WriteByte('[') |
| switch tStep.Key.Type() { |
| case cty.String: |
| ret.WriteString(fmt.Sprintf("%q", tStep.Key.AsString())) |
| case cty.Number: |
| bf := tStep.Key.AsBigFloat() |
| ret.WriteString(bf.Text('g', 10)) |
| } |
| ret.WriteByte(']') |
| } |
| } |
| return ret.String() |
| } |
| |
| // ParseRef attempts to extract a referencable address from the prefix of the |
| // given traversal, which must be an absolute traversal or this function |
| // will panic. |
| // |
| // If no error diagnostics are returned, the returned reference includes the |
| // address that was extracted, the source range it was extracted from, and any |
| // remaining relative traversal that was not consumed as part of the |
| // reference. |
| // |
| // If error diagnostics are returned then the Reference value is invalid and |
| // must not be used. |
| func ParseRef(traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) { |
| ref, diags := parseRef(traversal) |
| |
| // Normalize a little to make life easier for callers. |
| if ref != nil { |
| if len(ref.Remaining) == 0 { |
| ref.Remaining = nil |
| } |
| } |
| |
| return ref, diags |
| } |
| |
| // ParseRefStr is a helper wrapper around ParseRef that takes a string |
| // and parses it with the HCL native syntax traversal parser before |
| // interpreting it. |
| // |
| // This should be used only in specialized situations since it will cause the |
| // created references to not have any meaningful source location information. |
| // If a reference string is coming from a source that should be identified in |
| // error messages then the caller should instead parse it directly using a |
| // suitable function from the HCL API and pass the traversal itself to |
| // ParseRef. |
| // |
| // Error diagnostics are returned if either the parsing fails or the analysis |
| // of the traversal fails. There is no way for the caller to distinguish the |
| // two kinds of diagnostics programmatically. If error diagnostics are returned |
| // the returned reference may be nil or incomplete. |
| func ParseRefStr(str string) (*Reference, tfdiags.Diagnostics) { |
| var diags tfdiags.Diagnostics |
| |
| traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1}) |
| diags = diags.Append(parseDiags) |
| if parseDiags.HasErrors() { |
| return nil, diags |
| } |
| |
| ref, targetDiags := ParseRef(traversal) |
| diags = diags.Append(targetDiags) |
| return ref, diags |
| } |
| |
| func parseRef(traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) { |
| var diags tfdiags.Diagnostics |
| |
| root := traversal.RootName() |
| rootRange := traversal[0].SourceRange() |
| |
| switch root { |
| |
| case "count": |
| name, rng, remain, diags := parseSingleAttrRef(traversal) |
| return &Reference{ |
| Subject: CountAttr{Name: name}, |
| SourceRange: tfdiags.SourceRangeFromHCL(rng), |
| Remaining: remain, |
| }, diags |
| |
| case "each": |
| name, rng, remain, diags := parseSingleAttrRef(traversal) |
| return &Reference{ |
| Subject: ForEachAttr{Name: name}, |
| SourceRange: tfdiags.SourceRangeFromHCL(rng), |
| Remaining: remain, |
| }, diags |
| |
| case "data": |
| if len(traversal) < 3 { |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Invalid reference", |
| Detail: `The "data" object must be followed by two attribute names: the data source type and the resource name.`, |
| Subject: traversal.SourceRange().Ptr(), |
| }) |
| return nil, diags |
| } |
| remain := traversal[1:] // trim off "data" so we can use our shared resource reference parser |
| return parseResourceRef(DataResourceMode, rootRange, remain) |
| |
| case "resource": |
| // This is an alias for the normal case of just using a managed resource |
| // type as a top-level symbol, which will serve as an escape mechanism |
| // if a later edition of the Terraform language introduces a new |
| // reference prefix that conflicts with a resource type name in an |
| // existing provider. In that case, the edition upgrade tool can |
| // rewrite foo.bar into resource.foo.bar to ensure that "foo" remains |
| // interpreted as a resource type name rather than as the new reserved |
| // word. |
| if len(traversal) < 3 { |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Invalid reference", |
| Detail: `The "resource" object must be followed by two attribute names: the resource type and the resource name.`, |
| Subject: traversal.SourceRange().Ptr(), |
| }) |
| return nil, diags |
| } |
| remain := traversal[1:] // trim off "resource" so we can use our shared resource reference parser |
| return parseResourceRef(ManagedResourceMode, rootRange, remain) |
| |
| case "local": |
| name, rng, remain, diags := parseSingleAttrRef(traversal) |
| return &Reference{ |
| Subject: LocalValue{Name: name}, |
| SourceRange: tfdiags.SourceRangeFromHCL(rng), |
| Remaining: remain, |
| }, diags |
| |
| case "module": |
| callName, callRange, remain, diags := parseSingleAttrRef(traversal) |
| if diags.HasErrors() { |
| return nil, diags |
| } |
| |
| // A traversal starting with "module" can either be a reference to an |
| // entire module, or to a single output from a module instance, |
| // depending on what we find after this introducer. |
| callInstance := ModuleCallInstance{ |
| Call: ModuleCall{ |
| Name: callName, |
| }, |
| Key: NoKey, |
| } |
| |
| if len(remain) == 0 { |
| // Reference to an entire module. Might alternatively be a |
| // reference to a single instance of a particular module, but the |
| // caller will need to deal with that ambiguity since we don't have |
| // enough context here. |
| return &Reference{ |
| Subject: callInstance.Call, |
| SourceRange: tfdiags.SourceRangeFromHCL(callRange), |
| Remaining: remain, |
| }, diags |
| } |
| |
| if idxTrav, ok := remain[0].(hcl.TraverseIndex); ok { |
| var err error |
| callInstance.Key, err = ParseInstanceKey(idxTrav.Key) |
| if err != nil { |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Invalid index key", |
| Detail: fmt.Sprintf("Invalid index for module instance: %s.", err), |
| Subject: &idxTrav.SrcRange, |
| }) |
| return nil, diags |
| } |
| remain = remain[1:] |
| |
| if len(remain) == 0 { |
| // Also a reference to an entire module instance, but we have a key |
| // now. |
| return &Reference{ |
| Subject: callInstance, |
| SourceRange: tfdiags.SourceRangeFromHCL(hcl.RangeBetween(callRange, idxTrav.SrcRange)), |
| Remaining: remain, |
| }, diags |
| } |
| } |
| |
| if attrTrav, ok := remain[0].(hcl.TraverseAttr); ok { |
| remain = remain[1:] |
| return &Reference{ |
| Subject: ModuleCallInstanceOutput{ |
| Name: attrTrav.Name, |
| Call: callInstance, |
| }, |
| SourceRange: tfdiags.SourceRangeFromHCL(hcl.RangeBetween(callRange, attrTrav.SrcRange)), |
| Remaining: remain, |
| }, diags |
| } |
| |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Invalid reference", |
| Detail: "Module instance objects do not support this operation.", |
| Subject: remain[0].SourceRange().Ptr(), |
| }) |
| return nil, diags |
| |
| case "path": |
| name, rng, remain, diags := parseSingleAttrRef(traversal) |
| return &Reference{ |
| Subject: PathAttr{Name: name}, |
| SourceRange: tfdiags.SourceRangeFromHCL(rng), |
| Remaining: remain, |
| }, diags |
| |
| case "self": |
| return &Reference{ |
| Subject: Self, |
| SourceRange: tfdiags.SourceRangeFromHCL(rootRange), |
| Remaining: traversal[1:], |
| }, diags |
| |
| case "terraform": |
| name, rng, remain, diags := parseSingleAttrRef(traversal) |
| return &Reference{ |
| Subject: TerraformAttr{Name: name}, |
| SourceRange: tfdiags.SourceRangeFromHCL(rng), |
| Remaining: remain, |
| }, diags |
| |
| case "var": |
| name, rng, remain, diags := parseSingleAttrRef(traversal) |
| return &Reference{ |
| Subject: InputVariable{Name: name}, |
| SourceRange: tfdiags.SourceRangeFromHCL(rng), |
| Remaining: remain, |
| }, diags |
| |
| case "template", "lazy", "arg": |
| // These names are all pre-emptively reserved in the hope of landing |
| // some version of "template values" or "lazy expressions" feature |
| // before the next opt-in language edition, but don't yet do anything. |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Reserved symbol name", |
| Detail: fmt.Sprintf("The symbol name %q is reserved for use in a future Terraform version. If you are using a provider that already uses this as a resource type name, add the prefix \"resource.\" to force interpretation as a resource type name.", root), |
| Subject: rootRange.Ptr(), |
| }) |
| return nil, diags |
| |
| default: |
| return parseResourceRef(ManagedResourceMode, rootRange, traversal) |
| } |
| } |
| |
| func parseResourceRef(mode ResourceMode, startRange hcl.Range, traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) { |
| var diags tfdiags.Diagnostics |
| |
| if len(traversal) < 2 { |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Invalid reference", |
| Detail: `A reference to a resource type must be followed by at least one attribute access, specifying the resource name.`, |
| Subject: hcl.RangeBetween(traversal[0].SourceRange(), traversal[len(traversal)-1].SourceRange()).Ptr(), |
| }) |
| return nil, diags |
| } |
| |
| var typeName, name string |
| switch tt := traversal[0].(type) { // Could be either root or attr, depending on our resource mode |
| case hcl.TraverseRoot: |
| typeName = tt.Name |
| case hcl.TraverseAttr: |
| typeName = tt.Name |
| default: |
| // If it isn't a TraverseRoot then it must be a "data" reference. |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Invalid reference", |
| Detail: `The "data" object does not support this operation.`, |
| Subject: traversal[0].SourceRange().Ptr(), |
| }) |
| return nil, diags |
| } |
| |
| attrTrav, ok := traversal[1].(hcl.TraverseAttr) |
| if !ok { |
| var what string |
| switch mode { |
| case DataResourceMode: |
| what = "data source" |
| default: |
| what = "resource type" |
| } |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Invalid reference", |
| Detail: fmt.Sprintf(`A reference to a %s must be followed by at least one attribute access, specifying the resource name.`, what), |
| Subject: traversal[1].SourceRange().Ptr(), |
| }) |
| return nil, diags |
| } |
| name = attrTrav.Name |
| rng := hcl.RangeBetween(startRange, attrTrav.SrcRange) |
| remain := traversal[2:] |
| |
| resourceAddr := Resource{ |
| Mode: mode, |
| Type: typeName, |
| Name: name, |
| } |
| resourceInstAddr := ResourceInstance{ |
| Resource: resourceAddr, |
| Key: NoKey, |
| } |
| |
| if len(remain) == 0 { |
| // This might actually be a reference to the collection of all instances |
| // of the resource, but we don't have enough context here to decide |
| // so we'll let the caller resolve that ambiguity. |
| return &Reference{ |
| Subject: resourceAddr, |
| SourceRange: tfdiags.SourceRangeFromHCL(rng), |
| }, diags |
| } |
| |
| if idxTrav, ok := remain[0].(hcl.TraverseIndex); ok { |
| var err error |
| resourceInstAddr.Key, err = ParseInstanceKey(idxTrav.Key) |
| if err != nil { |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Invalid index key", |
| Detail: fmt.Sprintf("Invalid index for resource instance: %s.", err), |
| Subject: &idxTrav.SrcRange, |
| }) |
| return nil, diags |
| } |
| remain = remain[1:] |
| rng = hcl.RangeBetween(rng, idxTrav.SrcRange) |
| } |
| |
| return &Reference{ |
| Subject: resourceInstAddr, |
| SourceRange: tfdiags.SourceRangeFromHCL(rng), |
| Remaining: remain, |
| }, diags |
| } |
| |
| func parseSingleAttrRef(traversal hcl.Traversal) (string, hcl.Range, hcl.Traversal, tfdiags.Diagnostics) { |
| var diags tfdiags.Diagnostics |
| |
| root := traversal.RootName() |
| rootRange := traversal[0].SourceRange() |
| |
| if len(traversal) < 2 { |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Invalid reference", |
| Detail: fmt.Sprintf("The %q object cannot be accessed directly. Instead, access one of its attributes.", root), |
| Subject: &rootRange, |
| }) |
| return "", hcl.Range{}, nil, diags |
| } |
| if attrTrav, ok := traversal[1].(hcl.TraverseAttr); ok { |
| return attrTrav.Name, hcl.RangeBetween(rootRange, attrTrav.SrcRange), traversal[2:], diags |
| } |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Invalid reference", |
| Detail: fmt.Sprintf("The %q object does not support this operation.", root), |
| Subject: traversal[1].SourceRange().Ptr(), |
| }) |
| return "", hcl.Range{}, nil, diags |
| } |