Skip to content

Commit

Permalink
feat: use new standardized structures for errors
Browse files Browse the repository at this point in the history
This adds support for the new error formats designed for IBM Cloud
services and tools. The support is only added in select VPC resources/
data sources for now. The support comes from wrapping the provider to
catch errors and re-format them with the new system, so that the change
to the actual resource/data source code has as small of a surface area
as possible.

The primary change to resource code comes in the from of updating calls
to `fmt.Errorf` to be calls to a new wrapper function, `flex.FmtErrorf`.
It accepts the same arguments but instead of wrapping errors coming from
the underlying Go SDK so that we retain only the message, it detects those
errors and returns them so that we preserve all of the information needed
for the new error formats.

With this change, error messages will now include more information about the
problem and will include an identifying hash that allows the user to lookup
more information about the specific error scenario that occurred.

Signed-off-by: Dustin Popp <[email protected]>
  • Loading branch information
dpopp07 committed Mar 14, 2024
1 parent e75d10e commit 82a2207
Show file tree
Hide file tree
Showing 215 changed files with 2,173 additions and 2,029 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ require (
github.com/IBM/event-notifications-go-admin-sdk v0.4.0
github.com/IBM/eventstreams-go-sdk v1.4.0
github.com/IBM/go-sdk-core/v3 v3.2.4
github.com/IBM/go-sdk-core/v5 v5.15.3
github.com/IBM/go-sdk-core/v5 v5.16.1
github.com/IBM/ibm-cos-sdk-go v1.10.0
github.com/IBM/ibm-cos-sdk-go-config v1.2.0
github.com/IBM/ibm-hpcs-tke-sdk v0.0.0-20211109141421-a4b61b05f7d1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,8 @@ github.com/IBM/go-sdk-core/v5 v5.7.0/go.mod h1:+YbdhrjCHC84ls4MeBp+Hj4NZCni+tDAc
github.com/IBM/go-sdk-core/v5 v5.9.2/go.mod h1:YlOwV9LeuclmT/qi/LAK2AsobbAP42veV0j68/rlZsE=
github.com/IBM/go-sdk-core/v5 v5.9.5/go.mod h1:YlOwV9LeuclmT/qi/LAK2AsobbAP42veV0j68/rlZsE=
github.com/IBM/go-sdk-core/v5 v5.10.2/go.mod h1:WZPFasUzsKab/2mzt29xPcfruSk5js2ywAPwW4VJjdI=
github.com/IBM/go-sdk-core/v5 v5.15.3 h1:yBSSYLuQSO9Ip+j3mONsTcymoYQyxarQ6rh3aU9cVt8=
github.com/IBM/go-sdk-core/v5 v5.15.3/go.mod h1:ee+AZaB15yUwZigJdRCwZZ3u7HIvEQzxNUdxVpnJHY8=
github.com/IBM/go-sdk-core/v5 v5.16.1 h1:vAgOxRvaXD5AmgwR7dlstjT1JFE4BA4lPcGsEFZOKGs=
github.com/IBM/go-sdk-core/v5 v5.16.1/go.mod h1:aojBkkq4HXkOYdn7YZ6ve8cjPWHdcB3tt8v0b9Cbac8=
github.com/IBM/ibm-cos-sdk-go v1.3.1/go.mod h1:YLBAYobEA8bD27P7xpMwSQeNQu6W3DNBtBComXrRzRY=
github.com/IBM/ibm-cos-sdk-go v1.10.0 h1:/2VIev2/jBei39OqU2+nSZQnoWJ+KtkiSAIDkqsd7uU=
github.com/IBM/ibm-cos-sdk-go v1.10.0/go.mod h1:C8KRTRaoD3CWPPBOa6FCOpdh0ZMlUjKAAA4i3F+Q/sc=
Expand Down
2 changes: 2 additions & 0 deletions ibm/flex/structures.go
Original file line number Diff line number Diff line change
Expand Up @@ -3274,6 +3274,7 @@ type ServiceErrorResponse struct {
Message string
StatusCode int
Result interface{}
Error error
}

func BeautifyError(err error, response *core.DetailedResponse) *ServiceErrorResponse {
Expand All @@ -3289,6 +3290,7 @@ func BeautifyError(err error, response *core.DetailedResponse) *ServiceErrorResp
Message: err.Error(),
StatusCode: statusCode,
Result: result,
Error: err,
}
}

Expand Down
109 changes: 109 additions & 0 deletions ibm/flex/terraform_problem.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package flex

import (
"errors"
"fmt"
v "github.com/IBM-Cloud/terraform-provider-ibm/version"
"github.com/IBM/go-sdk-core/v5/core"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
)

// TerraformProblem provides a type that holds standardized information
// suitable to problems that occur in the Terraform Provider code.
type TerraformProblem struct {
*core.IBMProblem

Resource string
Operation string
}

// GetID returns a hash value computed from stable fields in the
// TerraformProblem instance, including Resource and Operation.
func (e *TerraformProblem) GetID() string {
return core.CreateIDHash("terraform", e.GetBaseSignature(), e.Resource, e.Operation)
}

// GetConsoleMessage returns the fields of the problem that
// are relevant to a user, formatted as a YAML string.
func (e *TerraformProblem) GetConsoleMessage() string {
return core.ComputeConsoleMessage(e)
}

// GetConsoleMessage returns the fields of the problem that
// are relevant to a developer, formatted as a YAML string.
func (e *TerraformProblem) GetDebugMessage() string {
return core.ComputeDebugMessage(e)
}

func (e *TerraformProblem) GetConsoleOrderedMaps() *core.OrderedMaps {
orderedMaps := core.NewOrderedMaps()

orderedMaps.Add("id", e.GetID())
orderedMaps.Add("summary", e.Summary)
orderedMaps.Add("severity", e.Severity)
orderedMaps.Add("resource", e.Resource)
orderedMaps.Add("operation", e.Operation)
orderedMaps.Add("component", e.Component)

return orderedMaps
}

func (e *TerraformProblem) GetDebugOrderedMaps() *core.OrderedMaps {
orderedMaps := e.GetConsoleOrderedMaps()

var orderableCausedBy core.OrderableProblem
if errors.As(e.GetCausedBy(), &orderableCausedBy) {
orderedMaps.Add("caused_by", orderableCausedBy.GetDebugOrderedMaps().GetMaps())
}

return orderedMaps
}

// GetDiag returns a new Diagnostics object using the console
// message as the summary. It is used to create a Diagnostics
// object from a TerraformProblem in the resource/data source code.
func (e *TerraformProblem) GetDiag() diag.Diagnostics {
return diag.Errorf("%s", e.GetConsoleMessage())
}

// TerraformErrorf creates and returns a new instance
// of `TerraformProblem` with "error" level severity.
func TerraformErrorf(err error, summary, resource, operation string) *TerraformProblem {
return &TerraformProblem{
IBMProblem: core.IBMErrorf(err, getComponentInfo(), summary, ""),
Resource: resource,
Operation: operation,
}
}

func getComponentInfo() *core.ProblemComponent {
return core.NewProblemComponent("github.com/IBM-Cloud/terraform-provider-ibm", v.Version)
}

// FmtErrorf wraps `fmt.Errorf(format string, a ...interface{}) error`
// and checks for the instance of a "Problem" type. If it finds one,
// the Problem instance needs to be returned instead of wrapped by
// `fmt.Errorf`.
func FmtErrorf(format string, a ...interface{}) error {
for _, arg := range a {
// Look for an error instance among the arguments.
var err error

if errArg, ok := arg.(error); ok {
err = errArg
} else if ser, ok := arg.(*ServiceErrorResponse); ok {
// Deal with the "ServiceErrorResponse" type, which
// wraps errors in some of the handwritten code.
err = ser.Error
}

if err != nil {
var problem core.Problem
if errors.As(err, &problem) {
return problem
}
}
}

return fmt.Errorf(format, a...)
}
151 changes: 150 additions & 1 deletion ibm/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@
package provider

import (
"context"
"errors"
"fmt"
"log"
"os"
"sync"
"time"

"github.com/IBM-Cloud/terraform-provider-ibm/ibm/conns"
"github.com/IBM-Cloud/terraform-provider-ibm/ibm/flex"
"github.com/IBM-Cloud/terraform-provider-ibm/ibm/service/apigateway"
"github.com/IBM-Cloud/terraform-provider-ibm/ibm/service/appconfiguration"
"github.com/IBM-Cloud/terraform-provider-ibm/ibm/service/appid"
Expand Down Expand Up @@ -54,12 +59,14 @@ import (
"github.com/IBM-Cloud/terraform-provider-ibm/ibm/service/usagereports"
"github.com/IBM-Cloud/terraform-provider-ibm/ibm/service/vpc"
"github.com/IBM-Cloud/terraform-provider-ibm/ibm/validate"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/IBM/go-sdk-core/v5/core"
)

// Provider returns a *schema.Provider.
func Provider() *schema.Provider {
return &schema.Provider{
provider := schema.Provider{
Schema: map[string]*schema.Schema{
"bluemix_api_key": {
Type: schema.TypeString,
Expand Down Expand Up @@ -1431,6 +1438,148 @@ func Provider() *schema.Provider {

ConfigureFunc: providerConfigure,
}

wrappedProvider := wrapProvider(provider)
return &wrappedProvider
}

func wrapProvider(provider schema.Provider) schema.Provider {
wrappedResourcesMap := map[string]*schema.Resource{}
wrappedDataSourcesMap := map[string]*schema.Resource{}

for key, value := range provider.ResourcesMap {
wrappedResourcesMap[key] = wrapResource(key, value)
}

for key, value := range provider.DataSourcesMap {
wrappedDataSourcesMap[key] = wrapDataSource(key, value)
}

return schema.Provider{
Schema: provider.Schema,
DataSourcesMap: wrappedDataSourcesMap,
ResourcesMap: wrappedResourcesMap,
ConfigureFunc: provider.ConfigureFunc,
}
}

func wrapResource(name string, resource *schema.Resource) *schema.Resource {
return &schema.Resource{
Schema: resource.Schema,
SchemaVersion: resource.SchemaVersion,
MigrateState: resource.MigrateState,
StateUpgraders: resource.StateUpgraders,
Exists: resource.Exists,
CreateContext: wrapFunction(name, "create", resource.CreateContext, resource.Create, false),
ReadContext: wrapFunction(name, "read", resource.ReadContext, resource.Read, false),
UpdateContext: wrapFunction(name, "update", resource.UpdateContext, resource.Update, false),
DeleteContext: wrapFunction(name, "delete", resource.DeleteContext, resource.Delete, false),
CreateWithoutTimeout: wrapFunction(name, "create", resource.CreateWithoutTimeout, nil, false),
ReadWithoutTimeout: wrapFunction(name, "read", resource.ReadWithoutTimeout, nil, false),
UpdateWithoutTimeout: wrapFunction(name, "update", resource.UpdateWithoutTimeout, nil, false),
DeleteWithoutTimeout: wrapFunction(name, "delete", resource.DeleteWithoutTimeout, nil, false),
CustomizeDiff: wrapCustomizeDiff(name, resource.CustomizeDiff),
Importer: resource.Importer,
DeprecationMessage: resource.DeprecationMessage,
Timeouts: resource.Timeouts,
Description: resource.Description,
UseJSONNumber: resource.UseJSONNumber,
}
}

func wrapDataSource(name string, resource *schema.Resource) *schema.Resource {
return &schema.Resource{
Schema: resource.Schema,
SchemaVersion: resource.SchemaVersion,
MigrateState: resource.MigrateState,
StateUpgraders: resource.StateUpgraders,
Exists: resource.Exists,
ReadContext: wrapFunction(name, "read", resource.ReadContext, resource.Read, true),
ReadWithoutTimeout: wrapFunction(name, "read", resource.ReadWithoutTimeout, nil, true),
Importer: resource.Importer,
DeprecationMessage: resource.DeprecationMessage,
Timeouts: resource.Timeouts,
Description: resource.Description,
UseJSONNumber: resource.UseJSONNumber,
}
}

func wrapFunction(
resourceName, operationName string,
function func(context.Context, *schema.ResourceData, interface{}) diag.Diagnostics,
fallback func(*schema.ResourceData, interface{}) error,
isDataSource bool,
) func(context.Context, *schema.ResourceData, interface{}) diag.Diagnostics {
if function != nil {
return func(context context.Context, schema *schema.ResourceData, meta interface{}) diag.Diagnostics {
return function(context, schema, meta)
}
} else if fallback != nil {
return func(context context.Context, schema *schema.ResourceData, meta interface{}) diag.Diagnostics {
return wrapError(fallback(schema, meta), resourceName, operationName, isDataSource)
}
}

return nil
}

func wrapError(err error, resourceName, operationName string, isDataSource bool) diag.Diagnostics {
var diags diag.Diagnostics

if err != nil {
// Distinguish data sources from resources. Data sources technically are resources but
// they may have the same names and we need to tell them apart.
if isDataSource {
resourceName = fmt.Sprintf("(Data) %s", resourceName)
}
tfError := flex.TerraformErrorf(err, "", resourceName, operationName)
log.Printf("[DEBUG] %s", tfError.GetDebugMessage())
return append(
diags,
diag.Diagnostic{
Severity: diag.Error,
Summary: tfError.Error(),
Detail: tfError.GetConsoleMessage(),
},
)
}

return nil
}

func wrapCustomizeDiff(resourceName string, function schema.CustomizeDiffFunc) schema.CustomizeDiffFunc {
if function == nil {
return nil
}

return func(c context.Context, rd *schema.ResourceDiff, i interface{}) error {
return wrapDiffErrors(function(c, rd, i), resourceName)
}
}

func wrapDiffErrors(err error, resourceName string) error {
if err != nil {
// CustomizeDiff fields often use the customizediff.All() method, which concatenates the errors
// returned from multiple functions using errors.Join(). Individual errors are still embedded in the
// error and can be extracted by unwrapping the error or using errors.As() (which is more convenient).
var problem core.Problem
if errors.As(err, &problem) {
// By the time this error gets printed by the Terraform code, we've lost control of it and the
// message that gets printed comes from the Error() method (and we only see the Summary).
// Although it would be ideal to return the full TerraformError object, it is sufficient
// to package the console message into a new error so that the user gets the information.
tfError := flex.TerraformErrorf(problem, "", resourceName, "CustomizeDiff")
log.Printf("[DEBUG] %s", tfError.GetDebugMessage())
return errors.New(tfError.GetConsoleMessage())
}

tfError := flex.TerraformErrorf(nil, err.Error(), resourceName, "CustomizeDiff")
log.Printf("[DEBUG] %s", tfError.GetDebugMessage())
return errors.New(tfError.GetConsoleMessage())
}

// Return the nil error.
return err
}

var (
Expand Down
5 changes: 2 additions & 3 deletions ibm/service/vpc/data_source_ibm_is_backup_policies.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package vpc

import (
"context"
"fmt"
"log"
"time"

Expand Down Expand Up @@ -278,7 +277,7 @@ func dataSourceIBMIsBackupPoliciesRead(context context.Context, d *schema.Resour
backupPolicyCollection, response, err := sess.ListBackupPoliciesWithContext(context, listBackupPoliciesOptions)
if err != nil {
log.Printf("[DEBUG] ListBackupPoliciesWithContext failed %s\n%s", err, response)
return diag.FromErr(fmt.Errorf("[ERROR] ListBackupPoliciesWithContext failed %s\n%s", err, response))
return diag.FromErr(flex.FmtErrorf("[ERROR] ListBackupPoliciesWithContext failed %s\n%s", err, response))
}
if backupPolicyCollection != nil && *backupPolicyCollection.TotalCount == int64(0) {
break
Expand All @@ -299,7 +298,7 @@ func dataSourceIBMIsBackupPoliciesRead(context context.Context, d *schema.Resour
if matchBackupPolicies != nil {
err = d.Set("backup_policies", dataSourceBackupPolicyCollectionFlattenBackupPolicies(matchBackupPolicies))
if err != nil {
return diag.FromErr(fmt.Errorf("[ERROR] Error setting backup_policies %s", err))
return diag.FromErr(flex.FmtErrorf("[ERROR] Error setting backup_policies %s", err))
}
}

Expand Down
Loading

0 comments on commit 82a2207

Please sign in to comment.