mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-12 09:01:58 -06:00
90ea7b0bc5
By observing the sorts of questions people ask in the community, and the ways they ask them, we've inferred that various different people have been confused by Terraform reporting that a value won't be known until apply or that a value is sensitive as part of an error message when that message doesn't actually relate to the known-ness and sensitivity of any value. Quite reasonably, someone who sees Terraform discussing an unfamiliar concept like unknown values can assume that it must be somehow relevant to the problem being discussed, and so in that sense Terraform's current error messages are giving "too much information": information that isn't actually helpful in understanding the problem being described, and in the worst case is a distraction from understanding the problem being described. With that in mind then, here we introduce an explicit annotation on diagnostic objects that are directly talking about unknown values or sensitive values, and then the diagnostic renderer will react to that to avoid using the terminology "known only after apply" or "sensitive" in the generated diagnostic annotations unless we're rendering a message that is explicitly related to one of those topics. This ends up being a bit of a cross-cutting concern because the code that generates these diagnostics and the code that renders them are in separate packages and are not directly aware of each other. With that in mind, the logic for actually deciding for a particular diagnostic whether it's flagged in one of these special ways lives inside the tfdiags package as an intermediation point, which both the diagnostic generator (in the core package) and the diagnostic renderer can both depend on.
172 lines
7.2 KiB
Go
172 lines
7.2 KiB
Go
package tfdiags
|
|
|
|
// This "Extra" idea is something we've inherited from HCL's diagnostic model,
|
|
// and so it's primarily to expose that functionality from wrapped HCL
|
|
// diagnostics but other diagnostic types could potentially implement this
|
|
// protocol too, if needed.
|
|
|
|
// ExtraInfo tries to retrieve extra information of interface type T from
|
|
// the given diagnostic.
|
|
//
|
|
// "Extra information" is situation-specific additional contextual data which
|
|
// might allow for some special tailored reporting of particular
|
|
// diagnostics in the UI. Conventionally the extra information is provided
|
|
// as a hidden type that implements one or more interfaces which a caller
|
|
// can pass as type parameter T to retrieve a value of that type when the
|
|
// diagnostic has such an implementation.
|
|
//
|
|
// If the given diagnostic's extra value has an implementation of interface T
|
|
// then ExtraInfo returns a non-nil interface value. If there is no such
|
|
// implementation, ExtraInfo returns a nil T.
|
|
//
|
|
// Although the signature of this function does not constrain T to be an
|
|
// interface type, our convention is to only use interface types to access
|
|
// extra info in order to allow for alternative or wrapping implementations
|
|
// of the interface.
|
|
func ExtraInfo[T any](diag Diagnostic) T {
|
|
extra := diag.ExtraInfo()
|
|
if ret, ok := extra.(T); ok {
|
|
return ret
|
|
}
|
|
|
|
// If "extra" doesn't implement T directly then we'll delegate to
|
|
// our ExtraInfoNext helper to try iteratively unwrapping it.
|
|
return ExtraInfoNext[T](extra)
|
|
}
|
|
|
|
// ExtraInfoNext takes a value previously returned by ExtraInfo and attempts
|
|
// to find an implementation of interface T wrapped inside of it. The return
|
|
// value meaning is the same as for ExtraInfo.
|
|
//
|
|
// This is to help with the less common situation where a particular "extra"
|
|
// value might be wrapping another value implementing the same interface,
|
|
// and so callers can peel away one layer at a time until there are no more
|
|
// nested layers.
|
|
//
|
|
// Because this function is intended for searching for _nested_ implementations
|
|
// of T, ExtraInfoNext does not consider whether value "previous" directly
|
|
// implements interface T, on the assumption that the previous call to ExtraInfo
|
|
// with the same T caused "previous" to already be that result.
|
|
func ExtraInfoNext[T any](previous interface{}) T {
|
|
// As long as T is an interface type as documented, zero will always be
|
|
// a nil interface value for us to return in the non-matching case.
|
|
var zero T
|
|
|
|
unwrapper, ok := previous.(DiagnosticExtraUnwrapper)
|
|
// If the given value isn't unwrappable then it can't possibly have
|
|
// any other info nested inside of it.
|
|
if !ok {
|
|
return zero
|
|
}
|
|
|
|
extra := unwrapper.UnwrapDiagnosticExtra()
|
|
|
|
// We'll keep unwrapping until we either find the interface we're
|
|
// looking for or we run out of layers of unwrapper.
|
|
for {
|
|
if ret, ok := extra.(T); ok {
|
|
return ret
|
|
}
|
|
|
|
if unwrapper, ok := extra.(DiagnosticExtraUnwrapper); ok {
|
|
extra = unwrapper.UnwrapDiagnosticExtra()
|
|
} else {
|
|
return zero
|
|
}
|
|
}
|
|
}
|
|
|
|
// DiagnosticExtraUnwrapper is an interface implemented by values in the
|
|
// Extra field of Diagnostic when they are wrapping another "Extra" value that
|
|
// was generated downstream.
|
|
//
|
|
// Diagnostic recipients which want to examine "Extra" values to sniff for
|
|
// particular types of extra data can either type-assert this interface
|
|
// directly and repeatedly unwrap until they recieve nil, or can use the
|
|
// helper function DiagnosticExtra.
|
|
//
|
|
// This interface intentionally matches hcl.DiagnosticExtraUnwrapper, so that
|
|
// wrapping extra values implemented using HCL's API will also work with the
|
|
// tfdiags API, but that non-HCL uses of this will not need to implement HCL
|
|
// just to get this interface.
|
|
type DiagnosticExtraUnwrapper interface {
|
|
// If the reciever is wrapping another "diagnostic extra" value, returns
|
|
// that value. Otherwise returns nil to indicate dynamically that nothing
|
|
// is wrapped.
|
|
//
|
|
// The "nothing is wrapped" condition can be signalled either by this
|
|
// method returning nil or by a type not implementing this interface at all.
|
|
//
|
|
// Implementers should never create unwrap "cycles" where a nested extra
|
|
// value returns a value that was also wrapping it.
|
|
UnwrapDiagnosticExtra() interface{}
|
|
}
|
|
|
|
// DiagnosticExtraBecauseUnknown is an interface implemented by values in
|
|
// the Extra field of Diagnostic when the diagnostic is potentially caused by
|
|
// the presence of unknown values in an expression evaluation.
|
|
//
|
|
// Just implementing this interface is not sufficient signal, though. Callers
|
|
// must also call the DiagnosticCausedByUnknown method in order to confirm
|
|
// the result, or use the package-level function DiagnosticCausedByUnknown
|
|
// as a convenient wrapper.
|
|
type DiagnosticExtraBecauseUnknown interface {
|
|
// DiagnosticCausedByUnknown returns true if the associated diagnostic
|
|
// was caused by the presence of unknown values during an expression
|
|
// evaluation, or false otherwise.
|
|
//
|
|
// Callers might use this to tailor what contextual information they show
|
|
// alongside an error report in the UI, to avoid potential confusion
|
|
// caused by talking about the presence of unknown values if that was
|
|
// immaterial to the error.
|
|
DiagnosticCausedByUnknown() bool
|
|
}
|
|
|
|
// DiagnosticCausedByUnknown returns true if the given diagnostic has an
|
|
// indication that it was caused by the presence of unknown values during
|
|
// an expression evaluation.
|
|
//
|
|
// This is a wrapper around checking if the diagnostic's extra info implements
|
|
// interface DiagnosticExtraBecauseUnknown and then calling its method if so.
|
|
func DiagnosticCausedByUnknown(diag Diagnostic) bool {
|
|
maybe := ExtraInfo[DiagnosticExtraBecauseUnknown](diag)
|
|
if maybe == nil {
|
|
return false
|
|
}
|
|
return maybe.DiagnosticCausedByUnknown()
|
|
}
|
|
|
|
// DiagnosticExtraBecauseSensitive is an interface implemented by values in
|
|
// the Extra field of Diagnostic when the diagnostic is potentially caused by
|
|
// the presence of sensitive values in an expression evaluation.
|
|
//
|
|
// Just implementing this interface is not sufficient signal, though. Callers
|
|
// must also call the DiagnosticCausedBySensitive method in order to confirm
|
|
// the result, or use the package-level function DiagnosticCausedBySensitive
|
|
// as a convenient wrapper.
|
|
type DiagnosticExtraBecauseSensitive interface {
|
|
// DiagnosticCausedBySensitive returns true if the associated diagnostic
|
|
// was caused by the presence of sensitive values during an expression
|
|
// evaluation, or false otherwise.
|
|
//
|
|
// Callers might use this to tailor what contextual information they show
|
|
// alongside an error report in the UI, to avoid potential confusion
|
|
// caused by talking about the presence of sensitive values if that was
|
|
// immaterial to the error.
|
|
DiagnosticCausedBySensitive() bool
|
|
}
|
|
|
|
// DiagnosticCausedBySensitive returns true if the given diagnostic has an
|
|
// indication that it was caused by the presence of sensitive values during
|
|
// an expression evaluation.
|
|
//
|
|
// This is a wrapper around checking if the diagnostic's extra info implements
|
|
// interface DiagnosticExtraBecauseSensitive and then calling its method if so.
|
|
func DiagnosticCausedBySensitive(diag Diagnostic) bool {
|
|
maybe := ExtraInfo[DiagnosticExtraBecauseSensitive](diag)
|
|
if maybe == nil {
|
|
return false
|
|
}
|
|
return maybe.DiagnosticCausedBySensitive()
|
|
}
|