2018-03-31 04:58:57 +02:00
|
|
|
package addrs
|
|
|
|
|
2018-04-24 01:58:01 +02:00
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"fmt"
|
|
|
|
|
2019-09-10 00:58:44 +02:00
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
2018-04-24 01:58:01 +02:00
|
|
|
"github.com/zclconf/go-cty/cty"
|
2018-06-08 02:20:28 +02:00
|
|
|
"github.com/zclconf/go-cty/cty/gocty"
|
2018-04-24 01:58:01 +02:00
|
|
|
|
|
|
|
"github.com/hashicorp/terraform/tfdiags"
|
|
|
|
)
|
2018-04-06 20:10:21 +02:00
|
|
|
|
2018-03-31 04:58:57 +02:00
|
|
|
// ModuleInstance is an address for a particular module instance within the
|
|
|
|
// dynamic module tree. This is an extension of the static traversals
|
|
|
|
// represented by type Module that deals with the possibility of a single
|
|
|
|
// module call producing multiple instances via the "count" and "for_each"
|
|
|
|
// arguments.
|
|
|
|
//
|
|
|
|
// Although ModuleInstance is a slice, it should be treated as immutable after
|
|
|
|
// creation.
|
|
|
|
type ModuleInstance []ModuleInstanceStep
|
|
|
|
|
2018-04-30 19:06:05 +02:00
|
|
|
var (
|
|
|
|
_ Targetable = ModuleInstance(nil)
|
|
|
|
)
|
|
|
|
|
2018-04-24 01:58:01 +02:00
|
|
|
func ParseModuleInstance(traversal hcl.Traversal) (ModuleInstance, tfdiags.Diagnostics) {
|
|
|
|
mi, remain, diags := parseModuleInstancePrefix(traversal)
|
|
|
|
if len(remain) != 0 {
|
|
|
|
if len(remain) == len(traversal) {
|
|
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
|
|
Severity: hcl.DiagError,
|
|
|
|
Summary: "Invalid module instance address",
|
|
|
|
Detail: "A module instance address must begin with \"module.\".",
|
|
|
|
Subject: remain.SourceRange().Ptr(),
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
|
|
Severity: hcl.DiagError,
|
|
|
|
Summary: "Invalid module instance address",
|
|
|
|
Detail: "The module instance address is followed by additional invalid content.",
|
|
|
|
Subject: remain.SourceRange().Ptr(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return mi, diags
|
|
|
|
}
|
|
|
|
|
2018-06-08 02:20:28 +02:00
|
|
|
// ParseModuleInstanceStr is a helper wrapper around ParseModuleInstance
|
|
|
|
// 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
|
2019-12-06 18:14:54 +01:00
|
|
|
// ParseModuleInstance.
|
2018-06-08 02:20:28 +02:00
|
|
|
//
|
|
|
|
// 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
|
|
|
|
// then the returned address is invalid.
|
|
|
|
func ParseModuleInstanceStr(str string) (ModuleInstance, 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
|
|
|
|
}
|
|
|
|
|
|
|
|
addr, addrDiags := ParseModuleInstance(traversal)
|
|
|
|
diags = diags.Append(addrDiags)
|
|
|
|
return addr, diags
|
|
|
|
}
|
|
|
|
|
2018-04-24 01:58:01 +02:00
|
|
|
func parseModuleInstancePrefix(traversal hcl.Traversal) (ModuleInstance, hcl.Traversal, tfdiags.Diagnostics) {
|
|
|
|
remain := traversal
|
|
|
|
var mi ModuleInstance
|
|
|
|
var diags tfdiags.Diagnostics
|
|
|
|
|
2020-12-01 15:05:59 +01:00
|
|
|
LOOP:
|
2018-04-24 01:58:01 +02:00
|
|
|
for len(remain) > 0 {
|
|
|
|
var next string
|
|
|
|
switch tt := remain[0].(type) {
|
|
|
|
case hcl.TraverseRoot:
|
|
|
|
next = tt.Name
|
|
|
|
case hcl.TraverseAttr:
|
|
|
|
next = tt.Name
|
|
|
|
default:
|
|
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
|
|
Severity: hcl.DiagError,
|
|
|
|
Summary: "Invalid address operator",
|
|
|
|
Detail: "Module address prefix must be followed by dot and then a name.",
|
|
|
|
Subject: remain[0].SourceRange().Ptr(),
|
|
|
|
})
|
2020-12-01 15:05:59 +01:00
|
|
|
break LOOP
|
2018-04-24 01:58:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if next != "module" {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
kwRange := remain[0].SourceRange()
|
|
|
|
remain = remain[1:]
|
|
|
|
// If we have the prefix "module" then we should be followed by an
|
|
|
|
// module call name, as an attribute, and then optionally an index step
|
|
|
|
// giving the instance key.
|
|
|
|
if len(remain) == 0 {
|
|
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
|
|
Severity: hcl.DiagError,
|
|
|
|
Summary: "Invalid address operator",
|
|
|
|
Detail: "Prefix \"module.\" must be followed by a module name.",
|
|
|
|
Subject: &kwRange,
|
|
|
|
})
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
var moduleName string
|
|
|
|
switch tt := remain[0].(type) {
|
|
|
|
case hcl.TraverseAttr:
|
|
|
|
moduleName = tt.Name
|
|
|
|
default:
|
|
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
|
|
Severity: hcl.DiagError,
|
|
|
|
Summary: "Invalid address operator",
|
|
|
|
Detail: "Prefix \"module.\" must be followed by a module name.",
|
|
|
|
Subject: remain[0].SourceRange().Ptr(),
|
|
|
|
})
|
2020-12-01 15:05:59 +01:00
|
|
|
break LOOP
|
2018-04-24 01:58:01 +02:00
|
|
|
}
|
|
|
|
remain = remain[1:]
|
|
|
|
step := ModuleInstanceStep{
|
|
|
|
Name: moduleName,
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(remain) > 0 {
|
|
|
|
if idx, ok := remain[0].(hcl.TraverseIndex); ok {
|
|
|
|
remain = remain[1:]
|
|
|
|
|
|
|
|
switch idx.Key.Type() {
|
|
|
|
case cty.String:
|
|
|
|
step.InstanceKey = StringKey(idx.Key.AsString())
|
|
|
|
case cty.Number:
|
|
|
|
var idxInt int
|
|
|
|
err := gocty.FromCtyValue(idx.Key, &idxInt)
|
|
|
|
if err == nil {
|
|
|
|
step.InstanceKey = IntKey(idxInt)
|
|
|
|
} else {
|
|
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
|
|
Severity: hcl.DiagError,
|
|
|
|
Summary: "Invalid address operator",
|
|
|
|
Detail: fmt.Sprintf("Invalid module index: %s.", err),
|
|
|
|
Subject: idx.SourceRange().Ptr(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
// Should never happen, because no other types are allowed in traversal indices.
|
|
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
|
|
Severity: hcl.DiagError,
|
|
|
|
Summary: "Invalid address operator",
|
|
|
|
Detail: "Invalid module key: must be either a string or an integer.",
|
|
|
|
Subject: idx.SourceRange().Ptr(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
mi = append(mi, step)
|
|
|
|
}
|
|
|
|
|
|
|
|
var retRemain hcl.Traversal
|
|
|
|
if len(remain) > 0 {
|
|
|
|
retRemain = make(hcl.Traversal, len(remain))
|
|
|
|
copy(retRemain, remain)
|
|
|
|
// The first element here might be either a TraverseRoot or a
|
|
|
|
// TraverseAttr, depending on whether we had a module address on the
|
|
|
|
// front. To make life easier for callers, we'll normalize to always
|
|
|
|
// start with a TraverseRoot.
|
|
|
|
if tt, ok := retRemain[0].(hcl.TraverseAttr); ok {
|
|
|
|
retRemain[0] = hcl.TraverseRoot{
|
|
|
|
Name: tt.Name,
|
|
|
|
SrcRange: tt.SrcRange,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return mi, retRemain, diags
|
|
|
|
}
|
|
|
|
|
2018-04-30 19:06:05 +02:00
|
|
|
// UnkeyedInstanceShim is a shim method for converting a Module address to the
|
|
|
|
// equivalent ModuleInstance address that assumes that no modules have
|
|
|
|
// keyed instances.
|
|
|
|
//
|
|
|
|
// This is a temporary allowance for the fact that Terraform does not presently
|
|
|
|
// support "count" and "for_each" on modules, and thus graph building code that
|
|
|
|
// derives graph nodes from configuration must just assume unkeyed modules
|
|
|
|
// in order to construct the graph. At a later time when "count" and "for_each"
|
|
|
|
// support is added for modules, all callers of this method will need to be
|
|
|
|
// reworked to allow for keyed module instances.
|
|
|
|
func (m Module) UnkeyedInstanceShim() ModuleInstance {
|
|
|
|
path := make(ModuleInstance, len(m))
|
|
|
|
for i, name := range m {
|
|
|
|
path[i] = ModuleInstanceStep{Name: name}
|
|
|
|
}
|
|
|
|
return path
|
|
|
|
}
|
|
|
|
|
2018-03-31 04:58:57 +02:00
|
|
|
// ModuleInstanceStep is a single traversal step through the dynamic module
|
|
|
|
// tree. It is used only as part of ModuleInstance.
|
|
|
|
type ModuleInstanceStep struct {
|
|
|
|
Name string
|
|
|
|
InstanceKey InstanceKey
|
|
|
|
}
|
|
|
|
|
|
|
|
// RootModuleInstance is the module instance address representing the root
|
|
|
|
// module, which is also the zero value of ModuleInstance.
|
|
|
|
var RootModuleInstance ModuleInstance
|
|
|
|
|
2018-04-30 19:06:05 +02:00
|
|
|
// IsRoot returns true if the receiver is the address of the root module instance,
|
|
|
|
// or false otherwise.
|
|
|
|
func (m ModuleInstance) IsRoot() bool {
|
|
|
|
return len(m) == 0
|
|
|
|
}
|
|
|
|
|
2018-03-31 04:58:57 +02:00
|
|
|
// Child returns the address of a child module instance of the receiver,
|
|
|
|
// identified by the given name and key.
|
|
|
|
func (m ModuleInstance) Child(name string, key InstanceKey) ModuleInstance {
|
|
|
|
ret := make(ModuleInstance, 0, len(m)+1)
|
|
|
|
ret = append(ret, m...)
|
|
|
|
return append(ret, ModuleInstanceStep{
|
|
|
|
Name: name,
|
|
|
|
InstanceKey: key,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parent returns the address of the parent module instance of the receiver, or
|
|
|
|
// the receiver itself if there is no parent (if it's the root module address).
|
|
|
|
func (m ModuleInstance) Parent() ModuleInstance {
|
|
|
|
if len(m) == 0 {
|
|
|
|
return m
|
|
|
|
}
|
|
|
|
return m[:len(m)-1]
|
|
|
|
}
|
2018-04-06 20:10:21 +02:00
|
|
|
|
|
|
|
// String returns a string representation of the receiver, in the format used
|
|
|
|
// within e.g. user-provided resource addresses.
|
|
|
|
//
|
|
|
|
// The address of the root module has the empty string as its representation.
|
|
|
|
func (m ModuleInstance) String() string {
|
|
|
|
var buf bytes.Buffer
|
|
|
|
sep := ""
|
|
|
|
for _, step := range m {
|
|
|
|
buf.WriteString(sep)
|
|
|
|
buf.WriteString("module.")
|
|
|
|
buf.WriteString(step.Name)
|
|
|
|
if step.InstanceKey != NoKey {
|
|
|
|
buf.WriteString(step.InstanceKey.String())
|
|
|
|
}
|
|
|
|
sep = "."
|
|
|
|
}
|
|
|
|
return buf.String()
|
|
|
|
}
|
2018-04-30 19:06:05 +02:00
|
|
|
|
2018-10-17 23:01:15 +02:00
|
|
|
// Equal returns true if the receiver and the given other value
|
|
|
|
// contains the exact same parts.
|
|
|
|
func (m ModuleInstance) Equal(o ModuleInstance) bool {
|
2021-03-05 18:07:31 +01:00
|
|
|
if len(m) != len(o) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
for i := range m {
|
|
|
|
if m[i] != o[i] {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
2018-10-17 23:01:15 +02:00
|
|
|
}
|
|
|
|
|
2018-06-08 02:17:47 +02:00
|
|
|
// Less returns true if the receiver should sort before the given other value
|
|
|
|
// in a sorted list of addresses.
|
|
|
|
func (m ModuleInstance) Less(o ModuleInstance) bool {
|
|
|
|
if len(m) != len(o) {
|
|
|
|
// Shorter path sorts first.
|
|
|
|
return len(m) < len(o)
|
|
|
|
}
|
|
|
|
|
|
|
|
for i := range m {
|
|
|
|
mS, oS := m[i], o[i]
|
|
|
|
switch {
|
|
|
|
case mS.Name != oS.Name:
|
|
|
|
return mS.Name < oS.Name
|
|
|
|
case mS.InstanceKey != oS.InstanceKey:
|
|
|
|
return InstanceKeyLess(mS.InstanceKey, oS.InstanceKey)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2018-04-30 19:06:05 +02:00
|
|
|
// Ancestors returns a slice containing the receiver and all of its ancestor
|
|
|
|
// module instances, all the way up to (and including) the root module.
|
|
|
|
// The result is ordered by depth, with the root module always first.
|
|
|
|
//
|
|
|
|
// Since the result always includes the root module, a caller may choose to
|
|
|
|
// ignore it by slicing the result with [1:].
|
|
|
|
func (m ModuleInstance) Ancestors() []ModuleInstance {
|
|
|
|
ret := make([]ModuleInstance, 0, len(m)+1)
|
|
|
|
for i := 0; i <= len(m); i++ {
|
|
|
|
ret = append(ret, m[:i])
|
|
|
|
}
|
|
|
|
return ret
|
|
|
|
}
|
|
|
|
|
2018-10-17 23:01:15 +02:00
|
|
|
// IsAncestor returns true if the receiver is an ancestor of the given
|
|
|
|
// other value.
|
|
|
|
func (m ModuleInstance) IsAncestor(o ModuleInstance) bool {
|
|
|
|
// Longer or equal sized paths means the receiver cannot
|
|
|
|
// be an ancestor of the given module insatnce.
|
|
|
|
if len(m) >= len(o) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
for i, ms := range m {
|
|
|
|
if ms.Name != o[i].Name {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if ms.InstanceKey != NoKey && ms.InstanceKey != o[i].InstanceKey {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2018-04-30 19:06:05 +02:00
|
|
|
// Call returns the module call address that corresponds to the given module
|
|
|
|
// instance, along with the address of the module instance that contains it.
|
|
|
|
//
|
|
|
|
// There is no call for the root module, so this method will panic if called
|
|
|
|
// on the root module address.
|
|
|
|
//
|
|
|
|
// A single module call can produce potentially many module instances, so the
|
|
|
|
// result discards any instance key that might be present on the last step
|
|
|
|
// of the instance. To retain this, use CallInstance instead.
|
|
|
|
//
|
|
|
|
// In practice, this just turns the last element of the receiver into a
|
|
|
|
// ModuleCall and then returns a slice of the receiever that excludes that
|
|
|
|
// last part. This is just a convenience for situations where a call address
|
|
|
|
// is required, such as when dealing with *Reference and Referencable values.
|
|
|
|
func (m ModuleInstance) Call() (ModuleInstance, ModuleCall) {
|
|
|
|
if len(m) == 0 {
|
|
|
|
panic("cannot produce ModuleCall for root module")
|
|
|
|
}
|
|
|
|
|
|
|
|
inst, lastStep := m[:len(m)-1], m[len(m)-1]
|
|
|
|
return inst, ModuleCall{
|
|
|
|
Name: lastStep.Name,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// CallInstance returns the module call instance address that corresponds to
|
|
|
|
// the given module instance, along with the address of the module instance
|
|
|
|
// that contains it.
|
|
|
|
//
|
|
|
|
// There is no call for the root module, so this method will panic if called
|
|
|
|
// on the root module address.
|
|
|
|
//
|
|
|
|
// In practice, this just turns the last element of the receiver into a
|
|
|
|
// ModuleCallInstance and then returns a slice of the receiever that excludes
|
|
|
|
// that last part. This is just a convenience for situations where a call\
|
|
|
|
// address is required, such as when dealing with *Reference and Referencable
|
|
|
|
// values.
|
|
|
|
func (m ModuleInstance) CallInstance() (ModuleInstance, ModuleCallInstance) {
|
|
|
|
if len(m) == 0 {
|
|
|
|
panic("cannot produce ModuleCallInstance for root module")
|
|
|
|
}
|
|
|
|
|
|
|
|
inst, lastStep := m[:len(m)-1], m[len(m)-1]
|
|
|
|
return inst, ModuleCallInstance{
|
|
|
|
Call: ModuleCall{
|
|
|
|
Name: lastStep.Name,
|
|
|
|
},
|
|
|
|
Key: lastStep.InstanceKey,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TargetContains implements Targetable by returning true if the given other
|
|
|
|
// address either matches the receiver, is a sub-module-instance of the
|
|
|
|
// receiver, or is a targetable absolute address within a module that
|
|
|
|
// is contained within the reciever.
|
|
|
|
func (m ModuleInstance) TargetContains(other Targetable) bool {
|
|
|
|
switch to := other.(type) {
|
2020-03-04 23:38:39 +01:00
|
|
|
case Module:
|
2018-04-30 19:06:05 +02:00
|
|
|
if len(to) < len(m) {
|
|
|
|
// Can't be contained if the path is shorter
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
// Other is contained if its steps match for the length of our own path.
|
|
|
|
for i, ourStep := range m {
|
|
|
|
otherStep := to[i]
|
2020-03-12 23:14:44 +01:00
|
|
|
|
|
|
|
// We can't contain an entire module if we have a specific instance
|
|
|
|
// key. The case of NoKey is OK because this address is either
|
|
|
|
// meant to address an unexpanded module, or a single instance of
|
|
|
|
// that module, and both of those are a covered in-full by the
|
|
|
|
// Module address.
|
|
|
|
if ourStep.InstanceKey != NoKey {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2020-03-04 23:38:39 +01:00
|
|
|
if ourStep.Name != otherStep {
|
2018-04-30 19:06:05 +02:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// If we fall out here then the prefixed matched, so it's contained.
|
|
|
|
return true
|
|
|
|
|
2020-03-04 23:38:39 +01:00
|
|
|
case ModuleInstance:
|
|
|
|
if len(to) < len(m) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for i, ourStep := range m {
|
|
|
|
otherStep := to[i]
|
2020-06-10 03:58:58 +02:00
|
|
|
|
|
|
|
if ourStep.Name != otherStep.Name {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// if this is our last step, because all targets are parsed as
|
|
|
|
// instances, this may be a ModuleInstance intended to be used as a
|
|
|
|
// Module.
|
|
|
|
if i == len(m)-1 {
|
|
|
|
if ourStep.InstanceKey == NoKey {
|
|
|
|
// If the other step is a keyed instance, then we contain that
|
|
|
|
// step, and if it isn't it's a match, which is true either way
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ourStep.InstanceKey != otherStep.InstanceKey {
|
2020-03-04 23:38:39 +01:00
|
|
|
return false
|
|
|
|
}
|
2020-06-10 03:58:58 +02:00
|
|
|
|
2020-03-04 23:38:39 +01:00
|
|
|
}
|
|
|
|
return true
|
|
|
|
|
2020-03-14 00:01:23 +01:00
|
|
|
case ConfigResource:
|
|
|
|
return m.TargetContains(to.Module)
|
|
|
|
|
2018-04-30 19:06:05 +02:00
|
|
|
case AbsResource:
|
|
|
|
return m.TargetContains(to.Module)
|
|
|
|
|
|
|
|
case AbsResourceInstance:
|
|
|
|
return m.TargetContains(to.Module)
|
|
|
|
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-22 03:41:38 +01:00
|
|
|
// Module returns the address of the module that this instance is an instance
|
|
|
|
// of.
|
|
|
|
func (m ModuleInstance) Module() Module {
|
|
|
|
if len(m) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
ret := make(Module, len(m))
|
|
|
|
for i, step := range m {
|
|
|
|
ret[i] = step.Name
|
|
|
|
}
|
|
|
|
return ret
|
|
|
|
}
|
|
|
|
|
2018-04-30 19:06:05 +02:00
|
|
|
func (m ModuleInstance) targetableSigil() {
|
|
|
|
// ModuleInstance is targetable
|
|
|
|
}
|
2019-11-22 00:37:44 +01:00
|
|
|
|
|
|
|
func (s ModuleInstanceStep) String() string {
|
|
|
|
if s.InstanceKey != NoKey {
|
|
|
|
return s.Name + s.InstanceKey.String()
|
|
|
|
}
|
|
|
|
return s.Name
|
|
|
|
}
|