From cfd1189bafa4f620325317ce3cc73edea8968051 Mon Sep 17 00:00:00 2001 From: Adrien Delorme Date: Wed, 23 Mar 2022 17:17:07 +0100 Subject: [PATCH] allow to reference datasources locals and builds --- hcl2template/addrs/instance_key.go | 82 ++++++++++++++++++++++ hcl2template/addrs/local_value.go | 11 +++ hcl2template/addrs/parse_ref.go | 109 +++++++++++++++++++++++++++++ hcl2template/addrs/resource.go | 74 ++++++++++++++++++++ 4 files changed, 276 insertions(+) create mode 100644 hcl2template/addrs/instance_key.go create mode 100644 hcl2template/addrs/local_value.go create mode 100644 hcl2template/addrs/resource.go diff --git a/hcl2template/addrs/instance_key.go b/hcl2template/addrs/instance_key.go new file mode 100644 index 000000000..484fbebfb --- /dev/null +++ b/hcl2template/addrs/instance_key.go @@ -0,0 +1,82 @@ +package addrs + +import ( + "fmt" + + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/gocty" +) + +// InstanceKey represents the key of an instance within an object that +// contains multiple instances due to using "count" or "for_each" arguments +// in configuration. +// +// IntKey and StringKey are the two implementations of this type. No other +// implementations are allowed. The single instance of an object that _isn't_ +// using "count" or "for_each" is represented by NoKey, which is a nil +// InstanceKey. +type InstanceKey interface { + instanceKeySigil() + String() string + + // Value returns the cty.Value of the appropriate type for the InstanceKey + // value. + Value() cty.Value +} + +// ParseInstanceKey returns the instance key corresponding to the given value, +// which must be known and non-null. +// +// If an unknown or null value is provided then this function will panic. This +// function is intended to deal with the values that would naturally be found +// in a hcl.TraverseIndex, which (when parsed from source, at least) can never +// contain unknown or null values. +func ParseInstanceKey(key cty.Value) (InstanceKey, error) { + switch key.Type() { + case cty.String: + return StringKey(key.AsString()), nil + case cty.Number: + var idx int + err := gocty.FromCtyValue(key, &idx) + return IntKey(idx), err + default: + return NoKey, fmt.Errorf("either a string or an integer is required") + } +} + +// NoKey represents the absense of an InstanceKey, for the single instance +// of a configuration object that does not use "count" or "for_each" at all. +var NoKey InstanceKey + +// IntKey is the InstanceKey representation representing integer indices, as +// used when the "count" argument is specified or if for_each is used with +// a sequence type. +type IntKey int + +func (k IntKey) instanceKeySigil() { +} + +func (k IntKey) String() string { + return fmt.Sprintf("[%d]", int(k)) +} + +func (k IntKey) Value() cty.Value { + return cty.NumberIntVal(int64(k)) +} + +// StringKey is the InstanceKey representation representing string indices, as +// used when the "for_each" argument is specified with a map or object type. +type StringKey string + +func (k StringKey) instanceKeySigil() { +} + +func (k StringKey) String() string { + // FIXME: This isn't _quite_ right because Go's quoted string syntax is + // slightly different than HCL's, but we'll accept it for now. + return fmt.Sprintf("[%q]", string(k)) +} + +func (k StringKey) Value() cty.Value { + return cty.StringVal(string(k)) +} diff --git a/hcl2template/addrs/local_value.go b/hcl2template/addrs/local_value.go new file mode 100644 index 000000000..c02036e0f --- /dev/null +++ b/hcl2template/addrs/local_value.go @@ -0,0 +1,11 @@ +package addrs + +// LocalValue is the address of a local value. +type LocalValue struct { + referenceable + Name string +} + +func (v LocalValue) String() string { + return "local." + v.Name +} diff --git a/hcl2template/addrs/parse_ref.go b/hcl2template/addrs/parse_ref.go index ee3f238ef..0543a6241 100644 --- a/hcl2template/addrs/parse_ref.go +++ b/hcl2template/addrs/parse_ref.go @@ -46,6 +46,14 @@ func parseRef(traversal hcl.Traversal) (*Reference, hcl.Diagnostics) { switch root { + case "local": + name, rng, remain, diags := parseSingleAttrRef(traversal) + return &Reference{ + Subject: LocalValue{Name: name}, + SourceRange: rng, + Remaining: remain, + }, diags + case "var": name, rng, remain, diags := parseSingleAttrRef(traversal) return &Reference{ @@ -54,6 +62,19 @@ func parseRef(traversal hcl.Traversal) (*Reference, hcl.Diagnostics) { 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) + default: diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, @@ -65,6 +86,94 @@ func parseRef(traversal hcl.Traversal) (*Reference, hcl.Diagnostics) { return nil, diags } +// parseResourceRef parses any kind of resource reference that is not a local or +// a var. It is handy to tell what is being referenced in a datasource, and in +// the future for a build. This function was taken from terraform core, hence +// why it is already refactored. +func parseResourceRef(mode ResourceMode, startRange hcl.Range, traversal hcl.Traversal) (*Reference, hcl.Diagnostics) { + var diags hcl.Diagnostics + + 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 = "build 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: 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: rng, + Remaining: remain, + }, diags +} + func parseSingleAttrRef(traversal hcl.Traversal) (string, hcl.Range, hcl.Traversal, hcl.Diagnostics) { var diags hcl.Diagnostics diff --git a/hcl2template/addrs/resource.go b/hcl2template/addrs/resource.go new file mode 100644 index 000000000..32fae5637 --- /dev/null +++ b/hcl2template/addrs/resource.go @@ -0,0 +1,74 @@ +package addrs + +import ( + "fmt" +) + +// Resource is an address for a resource block within configuration, which +// contains potentially-multiple resource instances if that configuration +// block uses "count" or "for_each". +type Resource struct { + referenceable + Mode ResourceMode + Type string + Name string +} + +func (r Resource) String() string { + switch r.Mode { + case DataResourceMode: + return fmt.Sprintf("data.%s.%s", r.Type, r.Name) + default: + // Should never happen, but we'll return a string here rather than + // crashing just in case it does. + return fmt.Sprintf(".%s.%s", r.Type, r.Name) + } +} + +func (r Resource) Equal(o Resource) bool { + return r.Mode == o.Mode && r.Name == o.Name && r.Type == o.Type +} + +// ResourceInstance is an address for a specific instance of a resource. +// When a resource is defined in configuration with "count" or "for_each" it +// produces zero or more instances, which can be addressed using this type. +type ResourceInstance struct { + referenceable + Resource Resource + Key InstanceKey +} + +func (r ResourceInstance) ContainingResource() Resource { + return r.Resource +} + +func (r ResourceInstance) String() string { + if r.Key == NoKey { + return r.Resource.String() + } + return r.Resource.String() + r.Key.String() +} + +func (r ResourceInstance) Equal(o ResourceInstance) bool { + return r.Key == o.Key && r.Resource.Equal(o.Resource) +} + +// ResourceMode defines which lifecycle applies to a given resource. Each +// resource lifecycle has a slightly different address format. +type ResourceMode rune + +//go:generate go run golang.org/x/tools/cmd/stringer -type ResourceMode + +const ( + // InvalidResourceMode is the zero value of ResourceMode and is not + // a valid resource mode. + InvalidResourceMode ResourceMode = 0 + + // BuildResourceMode indicates a build, as defined by "build" blocks in + // configuration. + BuildResourceMode ResourceMode = 'B' + + // DataResourceMode indicates a data resource, as defined by + // "data" blocks in configuration. + DataResourceMode ResourceMode = 'D' +)