github.com/crossplane/upjet@v1.3.0/pkg/terraform/errors/errors.go (about)

     1  // SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io>
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package errors
     6  
     7  import (
     8  	"fmt"
     9  	"strings"
    10  
    11  	jsoniter "github.com/json-iterator/go"
    12  	"github.com/pkg/errors"
    13  )
    14  
    15  const (
    16  	levelError = "error"
    17  )
    18  
    19  type tfError struct {
    20  	message string
    21  }
    22  
    23  type applyFailed struct {
    24  	*tfError
    25  }
    26  
    27  // TerraformLog represents relevant fields of a Terraform CLI JSON-formatted log line
    28  type TerraformLog struct {
    29  	Level      string        `json:"@level"`
    30  	Message    string        `json:"@message"`
    31  	Diagnostic LogDiagnostic `json:"diagnostic"`
    32  }
    33  
    34  // LogDiagnostic represents relevant fields of a Terraform CLI JSON-formatted
    35  // log line diagnostic info
    36  type LogDiagnostic struct {
    37  	Severity string `json:"severity"`
    38  	Summary  string `json:"summary"`
    39  	Detail   string `json:"detail"`
    40  }
    41  
    42  func (t *tfError) Error() string {
    43  	return t.message
    44  }
    45  
    46  func newTFError(message string, logs []byte) (string, *tfError) {
    47  	tfError := &tfError{
    48  		message: message,
    49  	}
    50  
    51  	tfLogs, err := parseTerraformLogs(logs)
    52  	if err != nil {
    53  		return err.Error(), tfError
    54  	}
    55  
    56  	messages := make([]string, 0, len(tfLogs))
    57  	for _, l := range tfLogs {
    58  		// only use error logs
    59  		if l == nil || l.Level != levelError {
    60  			continue
    61  		}
    62  		m := l.Message
    63  		if l.Diagnostic.Severity == levelError && l.Diagnostic.Summary != "" {
    64  			m = fmt.Sprintf("%s: %s", l.Diagnostic.Summary, l.Diagnostic.Detail)
    65  		}
    66  		messages = append(messages, m)
    67  	}
    68  	tfError.message = fmt.Sprintf("%s: %s", message, strings.Join(messages, "\n"))
    69  	return "", tfError
    70  }
    71  
    72  func parseTerraformLogs(logs []byte) ([]*TerraformLog, error) {
    73  	logLines := strings.Split(string(logs), "\n")
    74  	tfLogs := make([]*TerraformLog, 0, len(logLines))
    75  	for _, l := range logLines {
    76  		log := &TerraformLog{}
    77  		l := strings.TrimSpace(l)
    78  		if l == "" {
    79  			continue
    80  		}
    81  		if err := jsoniter.ConfigCompatibleWithStandardLibrary.UnmarshalFromString(l, log); err != nil {
    82  			return nil, err
    83  		}
    84  		tfLogs = append(tfLogs, log)
    85  	}
    86  	return tfLogs, nil
    87  }
    88  
    89  // NewApplyFailed returns a new apply failure error with given logs.
    90  func NewApplyFailed(logs []byte) error {
    91  	parseError, tfError := newTFError("apply failed", logs)
    92  	result := &applyFailed{tfError: tfError}
    93  	if parseError == "" {
    94  		return result
    95  	}
    96  	return errors.WithMessage(result, parseError)
    97  }
    98  
    99  // IsApplyFailed returns whether error is due to failure of an apply operation.
   100  func IsApplyFailed(err error) bool {
   101  	r := &applyFailed{}
   102  	return errors.As(err, &r)
   103  }
   104  
   105  type destroyFailed struct {
   106  	*tfError
   107  }
   108  
   109  // NewDestroyFailed returns a new destroy failure error with given logs.
   110  func NewDestroyFailed(logs []byte) error {
   111  	parseError, tfError := newTFError("destroy failed", logs)
   112  	result := &destroyFailed{tfError: tfError}
   113  	if parseError == "" {
   114  		return result
   115  	}
   116  	return errors.WithMessage(result, parseError)
   117  }
   118  
   119  // IsDestroyFailed returns whether error is due to failure of a destroy operation.
   120  func IsDestroyFailed(err error) bool {
   121  	r := &destroyFailed{}
   122  	return errors.As(err, &r)
   123  }
   124  
   125  type refreshFailed struct {
   126  	*tfError
   127  }
   128  
   129  // NewRefreshFailed returns a new destroy failure error with given logs.
   130  func NewRefreshFailed(logs []byte) error {
   131  	parseError, tfError := newTFError("refresh failed", logs)
   132  	result := &refreshFailed{tfError: tfError}
   133  	if parseError == "" {
   134  		return result
   135  	}
   136  	return errors.WithMessage(result, parseError)
   137  }
   138  
   139  // IsRefreshFailed returns whether error is due to failure of a destroy operation.
   140  func IsRefreshFailed(err error) bool {
   141  	r := &refreshFailed{}
   142  	return errors.As(err, &r)
   143  }
   144  
   145  type planFailed struct {
   146  	*tfError
   147  }
   148  
   149  // NewPlanFailed returns a new destroy failure error with given logs.
   150  func NewPlanFailed(logs []byte) error {
   151  	parseError, tfError := newTFError("plan failed", logs)
   152  	result := &planFailed{tfError: tfError}
   153  	if parseError == "" {
   154  		return result
   155  	}
   156  	return errors.WithMessage(result, parseError)
   157  }
   158  
   159  // IsPlanFailed returns whether error is due to failure of a destroy operation.
   160  func IsPlanFailed(err error) bool {
   161  	r := &planFailed{}
   162  	return errors.As(err, &r)
   163  }
   164  
   165  type retrySchedule struct {
   166  	invocationCount int
   167  	ttl             int
   168  }
   169  
   170  func NewRetryScheduleError(invocationCount, ttl int) error {
   171  	return &retrySchedule{
   172  		invocationCount: invocationCount,
   173  		ttl:             ttl,
   174  	}
   175  }
   176  
   177  func (r *retrySchedule) Error() string {
   178  	return fmt.Sprintf("native provider reuse budget has been exceeded: invocationCount: %d, ttl: %d", r.invocationCount, r.ttl)
   179  }
   180  
   181  // IsRetryScheduleError returns whether the error is a retry error
   182  // for the scheduler.
   183  func IsRetryScheduleError(err error) bool {
   184  	r := &retrySchedule{}
   185  	return errors.As(err, &r)
   186  }
   187  
   188  type asyncCreateFailed struct {
   189  	error
   190  }
   191  
   192  // NewAsyncCreateFailed returns a new async crate failure.
   193  func NewAsyncCreateFailed(err error) error {
   194  	if err == nil {
   195  		return nil
   196  	}
   197  	return &asyncCreateFailed{
   198  		error: errors.Wrap(err, "async create failed"),
   199  	}
   200  }
   201  
   202  // IsAsyncCreateFailed returns whether error is due to failure of
   203  // an async create operation.
   204  func IsAsyncCreateFailed(err error) bool {
   205  	r := &asyncCreateFailed{}
   206  	return errors.As(err, &r)
   207  }
   208  
   209  type asyncUpdateFailed struct {
   210  	error
   211  }
   212  
   213  // NewAsyncUpdateFailed returns a new async update failure.
   214  func NewAsyncUpdateFailed(err error) error {
   215  	if err == nil {
   216  		return nil
   217  	}
   218  	return &asyncUpdateFailed{
   219  		error: errors.Wrap(err, "async update failed"),
   220  	}
   221  }
   222  
   223  // IsAsyncUpdateFailed returns whether error is due to failure of
   224  // an async update operation.
   225  func IsAsyncUpdateFailed(err error) bool {
   226  	r := &asyncUpdateFailed{}
   227  	return errors.As(err, &r)
   228  }
   229  
   230  type asyncDeleteFailed struct {
   231  	error
   232  }
   233  
   234  // NewAsyncDeleteFailed returns a new async delete failure.
   235  func NewAsyncDeleteFailed(err error) error {
   236  	if err == nil {
   237  		return nil
   238  	}
   239  	return &asyncDeleteFailed{
   240  		error: errors.Wrap(err, "async delete failed"),
   241  	}
   242  }
   243  
   244  // IsAsyncDeleteFailed returns whether error is due to failure of
   245  // an async delete operation.
   246  func IsAsyncDeleteFailed(err error) bool {
   247  	r := &asyncDeleteFailed{}
   248  	return errors.As(err, &r)
   249  }