opentofu/command/format/object_id.go
Martin Atkins 275a44f552 command: Reinstate object ids in the UIHook progress logs
We used to treat the "id" attribute of a resource as special and elevate
it into its own struct field "ID" in the state, but the new state format
and provider protocol treats it just as any other attribute.

However, it's still useful to show the value of a single identifying
attribute when there isn't room in the UI for showing all of the
attributes, and so here we take a new strategy of considering "id" along
with some other conventional names as special only in the UI layer.

This new heuristic approach can be adjusted over time as new provider
patterns emerge, but for now it covers some common conventions we've seen
in real providers.

With that said, since all existing providers made for Terraform versions
prior to v0.12 were forced to set "id", we won't see any use of other
attributes here until providers are updated to remove the placeholder
ids they were generating in cases where an id was not actually relevant
but was forced by the old protocol. At that point the UX should be
improved by showing a more relevant attribute instead.

We now also allow for the possibility of no id at all, since that is valid
for resources that exist only within the Terraform state, like the ones
from the "random" and "tls" providers.
2018-10-16 19:14:11 -07:00

124 lines
4.1 KiB
Go

package format
import (
"github.com/zclconf/go-cty/cty"
)
// ObjectValueID takes a value that is assumed to be an object representation
// of some resource instance object and attempts to heuristically find an
// attribute of it that is likely to be a unique identifier in the remote
// system that it belongs to which will be useful to the user.
//
// If such an attribute is found, its name and string value intended for
// display are returned. Both returned strings are empty if no such attribute
// exists, in which case the caller should assume that the resource instance
// address within the Terraform configuration is the best available identifier.
//
// This is only a best-effort sort of thing, relying on naming conventions in
// our resource type schemas. The result is not guaranteed to be unique, but
// should generally be suitable for display to an end-user anyway.
//
// This function will panic if the given value is not of an object type.
func ObjectValueID(obj cty.Value) (k, v string) {
if obj.IsNull() || !obj.IsKnown() {
return "", ""
}
atys := obj.Type().AttributeTypes()
switch {
case atys["id"] == cty.String:
v := obj.GetAttr("id")
if v.IsKnown() && !v.IsNull() {
return "id", v.AsString()
}
case atys["name"] == cty.String:
// "name" isn't always globally unique, but if there isn't also an
// "id" then it _often_ is, in practice.
v := obj.GetAttr("name")
if v.IsKnown() && !v.IsNull() {
return "name", v.AsString()
}
}
return "", ""
}
// ObjectValueName takes a value that is assumed to be an object representation
// of some resource instance object and attempts to heuristically find an
// attribute of it that is likely to be a human-friendly name in the remote
// system that it belongs to which will be useful to the user.
//
// If such an attribute is found, its name and string value intended for
// display are returned. Both returned strings are empty if no such attribute
// exists, in which case the caller should assume that the resource instance
// address within the Terraform configuration is the best available identifier.
//
// This is only a best-effort sort of thing, relying on naming conventions in
// our resource type schemas. The result is not guaranteed to be unique, but
// should generally be suitable for display to an end-user anyway.
//
// Callers that use both ObjectValueName and ObjectValueID at the same time
// should be prepared to get the same attribute key and value from both in
// some cases, since there is overlap betweek the id-extraction and
// name-extraction heuristics.
//
// This function will panic if the given value is not of an object type.
func ObjectValueName(obj cty.Value) (k, v string) {
if obj.IsNull() || !obj.IsKnown() {
return "", ""
}
atys := obj.Type().AttributeTypes()
switch {
case atys["name"] == cty.String:
v := obj.GetAttr("name")
if v.IsKnown() && !v.IsNull() {
return "name", v.AsString()
}
case atys["tags"].IsMapType() && atys["tags"].ElementType() == cty.String:
tags := obj.GetAttr("tags")
if tags.IsNull() || !tags.IsWhollyKnown() {
break
}
switch {
case tags.HasIndex(cty.StringVal("name")).RawEquals(cty.True):
v := tags.Index(cty.StringVal("name"))
if v.IsKnown() && !v.IsNull() {
return "tags.name", v.AsString()
}
case tags.HasIndex(cty.StringVal("Name")).RawEquals(cty.True):
// AWS-style naming convention
v := tags.Index(cty.StringVal("Name"))
if v.IsKnown() && !v.IsNull() {
return "tags.Name", v.AsString()
}
}
}
return "", ""
}
// ObjectValueIDOrName is a convenience wrapper around both ObjectValueID
// and ObjectValueName (in that preference order) to try to extract some sort
// of human-friendly descriptive string value for an object as additional
// context about an object when it is being displayed in a compact way (where
// not all of the attributes are visible.)
//
// Just as with the two functions it wraps, it is a best-effort and may return
// two empty strings if no suitable attribute can be found for a given object.
func ObjectValueIDOrName(obj cty.Value) (k, v string) {
k, v = ObjectValueID(obj)
if k != "" {
return
}
k, v = ObjectValueName(obj)
return
}