mirror of https://github.com/hashicorp/terraform
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
310 lines
12 KiB
310 lines
12 KiB
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
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{}
|
|
}
|
|
|
|
// DiagnosticExtraWrapper is an interface implemented by values that can be
|
|
// dynamically updated to wrap other extra info.
|
|
type DiagnosticExtraWrapper interface {
|
|
// WrapDiagnosticExtra accepts an ExtraInfo that it should add within the
|
|
// current ExtraInfo.
|
|
WrapDiagnosticExtra(inner 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()
|
|
}
|
|
|
|
// DiagnosticExtraBecauseEphemeral is an interface implemented by values in
|
|
// the Extra field of Diagnostic when the diagnostic is potentially caused by
|
|
// the presence of ephemeral values in an expression evaluation.
|
|
//
|
|
// Just implementing this interface is not sufficient signal, though. Callers
|
|
// must also call the DiagnosticCausedByEphemeral method in order to confirm
|
|
// the result, or use the package-level function DiagnosticCausedByEphemeral
|
|
// as a convenient wrapper.
|
|
type DiagnosticExtraBecauseEphemeral interface {
|
|
// DiagnosticCausedByEphemeral returns true if the associated diagnostic
|
|
// was caused by the presence of ephemeral 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 deferred values if that was
|
|
// immaterial to the error.
|
|
DiagnosticCausedByEphemeral() bool
|
|
}
|
|
|
|
// DiagnosticCausedByEphemeral returns true if the given diagnostic has an
|
|
// indication that it was caused by the presence of deferred values during
|
|
// an expression evaluation.
|
|
//
|
|
// This is a wrapper around checking if the diagnostic's extra info implements
|
|
// interface DiagnosticExtraBecauseDeferred and then calling its method if so.
|
|
func DiagnosticCausedByEphemeral(diag Diagnostic) bool {
|
|
maybe := ExtraInfo[DiagnosticExtraBecauseEphemeral](diag)
|
|
if maybe == nil {
|
|
return false
|
|
}
|
|
return maybe.DiagnosticCausedByEphemeral()
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
|
|
// DiagnosticExtraDoNotConsolidate tells the Diagnostics.ConsolidateWarnings
|
|
// function not to consolidate this diagnostic if it otherwise would.
|
|
type DiagnosticExtraDoNotConsolidate interface {
|
|
// DoNotConsolidateDiagnostic returns true if the associated diagnostic
|
|
// should not be consolidated by the Diagnostics.ConsolidateWarnings
|
|
// function.
|
|
DoNotConsolidateDiagnostic() bool
|
|
}
|
|
|
|
// DoNotConsolidateDiagnostic returns true if the given diagnostic should not
|
|
// be consolidated by the Diagnostics.ConsolidateWarnings function.
|
|
func DoNotConsolidateDiagnostic(diag Diagnostic) bool {
|
|
maybe := ExtraInfo[DiagnosticExtraDoNotConsolidate](diag)
|
|
if maybe == nil {
|
|
return false
|
|
}
|
|
return maybe.DoNotConsolidateDiagnostic()
|
|
}
|
|
|
|
// DiagnosticExtraCausedByTestFailure is an interface implemented by
|
|
// values in the Extra field of Diagnostic when the diagnostic is caused by a
|
|
// failing assertion in a run block during the `test` command.
|
|
//
|
|
// Just implementing this interface is not sufficient signal, though. Callers
|
|
// must also call the DiagnosticCausedByTestFailure method in order to
|
|
// confirm the result, or use the package-level function
|
|
// DiagnosticCausedByTestFailure as a convenient wrapper.
|
|
type DiagnosticExtraCausedByTestFailure interface {
|
|
// DiagnosticCausedByTestFailure returns true if the associated
|
|
// diagnostic is the result of a failed assertion in a run block.
|
|
DiagnosticCausedByTestFailure() bool
|
|
|
|
// IsTestVerboseMode returns true if the test was executed in verbose mode.
|
|
IsTestVerboseMode() bool
|
|
}
|
|
|
|
// DiagnosticCausedByTestFailure returns true if the given diagnostic
|
|
// is the result of a failed assertion in a run block.
|
|
func DiagnosticCausedByTestFailure(diag Diagnostic) bool {
|
|
maybe := ExtraInfo[DiagnosticExtraCausedByTestFailure](diag)
|
|
if maybe == nil {
|
|
return false
|
|
}
|
|
return maybe.DiagnosticCausedByTestFailure()
|
|
}
|
|
|
|
// DiagnosticExtraDeprecationOrigin is an interface implemented by values in
|
|
// the Extra field of Diagnostic when the diagnostic is related to a
|
|
// deprecation warning. It provides information about the origin of the
|
|
// deprecation.
|
|
type DiagnosticExtraDeprecationOrigin interface {
|
|
DeprecationOrigin() *SourceRange
|
|
}
|
|
|
|
// DiagnosticDeprecationOrigin returns the origin range of a deprecation
|
|
// warning diagnostic, or nil if the diagnostic does not have such information.
|
|
func DiagnosticDeprecationOrigin(diag Diagnostic) *SourceRange {
|
|
maybe := ExtraInfo[DiagnosticExtraDeprecationOrigin](diag)
|
|
if maybe == nil {
|
|
return nil
|
|
}
|
|
return maybe.DeprecationOrigin()
|
|
}
|
|
|
|
type DeprecationOriginDiagnosticExtra struct {
|
|
Origin *SourceRange
|
|
|
|
wrapped interface{}
|
|
}
|
|
|
|
var (
|
|
_ DiagnosticExtraDeprecationOrigin = (*DeprecationOriginDiagnosticExtra)(nil)
|
|
_ DiagnosticExtraWrapper = (*DeprecationOriginDiagnosticExtra)(nil)
|
|
_ DiagnosticExtraUnwrapper = (*DeprecationOriginDiagnosticExtra)(nil)
|
|
)
|
|
|
|
func (c *DeprecationOriginDiagnosticExtra) UnwrapDiagnosticExtra() interface{} {
|
|
return c.wrapped
|
|
}
|
|
|
|
func (c *DeprecationOriginDiagnosticExtra) WrapDiagnosticExtra(inner interface{}) {
|
|
if c.wrapped != nil {
|
|
// This is a logical inconsistency, the caller should know whether they
|
|
// have already wrapped an extra or not.
|
|
panic("Attempted to wrap a diagnostic extra into a DeprecationOriginDiagnosticExtra that is already wrapping a different extra. This is a bug in Terraform, please report it.")
|
|
}
|
|
c.wrapped = inner
|
|
}
|
|
|
|
func (c *DeprecationOriginDiagnosticExtra) DeprecationOrigin() *SourceRange {
|
|
return c.Origin
|
|
}
|