github.com/SamarSidharth/kpt@v0.0.0-20231122062228-c7d747ae3ace/internal/errors/errors.go (about)

     1  // Copyright 2021 The kpt Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package errors defines the error handling used by kpt codebase.
    16  package errors
    17  
    18  import (
    19  	"fmt"
    20  	"strings"
    21  
    22  	goerrors "errors"
    23  
    24  	"github.com/GoogleContainerTools/kpt/internal/types"
    25  	kyaml_errors "github.com/go-errors/errors"
    26  )
    27  
    28  // Error is the type that implements error interface used in the kpt codebase.
    29  // It is based on the design in https://commandcenter.blogspot.com/2017/12/error-handling-in-upspin.html
    30  // The intent is to capture error information in a structured format so
    31  // that we can display it differently to different users for ex. kpt developers
    32  // are interested in operational trace along with more diagnostic information while
    33  // kpt end-users may be just interested in a concise and actionable information.
    34  // Representing errors in structured format helps us decouple the error information
    35  // from how it is surfaced to the end users.
    36  type Error struct {
    37  	// Path is the path of the object (pkg, file) involved in kpt operation.
    38  	Path types.UniquePath
    39  
    40  	// Op is the operation being performed, for ex. pkg.get, fn.render
    41  	Op Op
    42  
    43  	// Fn is the kpt function being run either as part of "fn render" or "fn eval"
    44  	Fn Fn
    45  
    46  	// Repo is the git repo used for get, update or diff
    47  	Repo Repo
    48  
    49  	// Class refers to class of errors
    50  	Class Class
    51  
    52  	// Err refers to wrapped error (if any)
    53  	Err error
    54  }
    55  
    56  func (e *Error) Error() string {
    57  	b := new(strings.Builder)
    58  
    59  	if e.Op != "" {
    60  		pad(b, ": ")
    61  		b.WriteString(string(e.Op))
    62  	}
    63  
    64  	if e.Path != "" {
    65  		pad(b, ": ")
    66  		b.WriteString("pkg ")
    67  		// try to print relative path of the pkg if we can else use abs path
    68  		relPath, err := e.Path.RelativePath()
    69  		if err != nil {
    70  			relPath = string(e.Path)
    71  		}
    72  		b.WriteString(relPath)
    73  	}
    74  
    75  	if e.Fn != "" {
    76  		pad(b, ": ")
    77  		b.WriteString("fn ")
    78  		b.WriteString(string(e.Fn))
    79  	}
    80  
    81  	if e.Repo != "" {
    82  		pad(b, ": ")
    83  		b.WriteString("repo ")
    84  		b.WriteString(string(e.Repo))
    85  	}
    86  
    87  	if e.Class != 0 {
    88  		pad(b, ": ")
    89  		b.WriteString(e.Class.String())
    90  	}
    91  
    92  	if e.Err != nil {
    93  		var wrappedErr *Error
    94  		if As(e.Err, &wrappedErr) {
    95  			if !wrappedErr.Zero() {
    96  				pad(b, ":\n\t")
    97  				b.WriteString(wrappedErr.Error())
    98  			}
    99  		} else {
   100  			pad(b, ": ")
   101  			b.WriteString(e.Err.Error())
   102  		}
   103  	}
   104  	if b.Len() == 0 {
   105  		return "no error"
   106  	}
   107  	return b.String()
   108  }
   109  
   110  // pad appends given str to the string buffer.
   111  func pad(b *strings.Builder, str string) {
   112  	if b.Len() == 0 {
   113  		return
   114  	}
   115  	b.WriteString(str)
   116  }
   117  
   118  func (e *Error) Zero() bool {
   119  	return e.Op == "" && e.Path == "" && e.Fn == "" && e.Class == 0 && e.Err == nil
   120  }
   121  
   122  func (e *Error) Unwrap() error {
   123  	return e.Err
   124  }
   125  
   126  // Op describes the operation being performed.
   127  type Op string
   128  
   129  // Fn describes the kpt function associated with the error.
   130  type Fn string
   131  
   132  // Repo describes the repo associated with the error.
   133  type Repo string
   134  
   135  // Class describes the class of errors encountered.
   136  type Class int
   137  
   138  const (
   139  	Other        Class = iota // Unclassified. Will not be printed.
   140  	Exist                     // Item already exists.
   141  	Internal                  // Internal error.
   142  	InvalidParam              // Value is not valid.
   143  	MissingParam              // Required value is missing or empty.
   144  	Git                       // Errors from Git
   145  	IO                        // Error doing IO operations
   146  	YAML                      // yaml document can't be parsed
   147  )
   148  
   149  func (c Class) String() string {
   150  	switch c {
   151  	case Other:
   152  		return "other error"
   153  	case Exist:
   154  		return "item already exist"
   155  	case Internal:
   156  		return "internal error"
   157  	case InvalidParam:
   158  		return "invalid parameter value"
   159  	case MissingParam:
   160  		return "missing parameter value"
   161  	case Git:
   162  		return "git error"
   163  	case IO:
   164  		return "IO error"
   165  	case YAML:
   166  		return "yaml parsing error"
   167  	}
   168  	return "unknown kind"
   169  }
   170  
   171  func E(args ...interface{}) error {
   172  	if len(args) == 0 {
   173  		panic("errors.E must have at least one argument")
   174  	}
   175  
   176  	e := &Error{}
   177  	for _, arg := range args {
   178  		switch a := arg.(type) {
   179  		case types.UniquePath:
   180  			e.Path = a
   181  		case Op:
   182  			e.Op = a
   183  		case Fn:
   184  			e.Fn = a
   185  		case Repo:
   186  			e.Repo = a
   187  		case Class:
   188  			e.Class = a
   189  		case *Error:
   190  			cp := *a
   191  			e.Err = &cp
   192  		case error:
   193  			e.Err = a
   194  		case string:
   195  			e.Err = fmt.Errorf(a)
   196  		default:
   197  			panic(fmt.Errorf("unknown type %T for value %v in call to error.E", a, a))
   198  		}
   199  	}
   200  	wrappedErr, ok := e.Err.(*Error)
   201  	if !ok {
   202  		return e
   203  	}
   204  	if e.Path == wrappedErr.Path {
   205  		wrappedErr.Path = ""
   206  	}
   207  
   208  	if e.Op == wrappedErr.Op {
   209  		wrappedErr.Op = ""
   210  	}
   211  
   212  	if e.Fn == wrappedErr.Fn {
   213  		wrappedErr.Fn = ""
   214  	}
   215  
   216  	if e.Repo == wrappedErr.Repo {
   217  		wrappedErr.Repo = ""
   218  	}
   219  
   220  	if e.Class == wrappedErr.Class {
   221  		wrappedErr.Class = 0
   222  	}
   223  	return e
   224  }
   225  
   226  // Is reports whether any error in err's chain matches target.
   227  func Is(err, target error) bool {
   228  	return goerrors.Is(err, target)
   229  }
   230  
   231  // As finds the first error in err's chain that matches target, and if so, sets
   232  // target to that error value and returns true. Otherwise, it returns false.
   233  func As(err error, target interface{}) bool {
   234  	return goerrors.As(err, target)
   235  }
   236  
   237  // UnwrapKioError unwraps the error returned by kio pipeline.
   238  // The error returned by kio pipeline is wrapped by
   239  // library 'github.com/go-errors/errors' and it doesn't
   240  // support 'Unwrap' method so 'errors.As' will not work.
   241  // This function will return the first error wrapped by kio
   242  // pipeline. If the error is not wrapped by kio pipeline, it
   243  // will return the original error.
   244  func UnwrapKioError(err error) error {
   245  	var kioErr *kyaml_errors.Error
   246  	if !goerrors.As(err, &kioErr) {
   247  		return err
   248  	}
   249  	return kioErr.Err
   250  }
   251  
   252  // UnwrapErrors unwraps any *Error errors in the chain and returns
   253  // the first error it finds that is of a different type. If no such error
   254  // can be found, the last return value will be false.
   255  // nolint
   256  func UnwrapErrors(err error) (error, bool) {
   257  	for {
   258  		if err == nil {
   259  			return nil, false
   260  		}
   261  		e, ok := err.(*Error)
   262  		if !ok {
   263  			return err, true
   264  		}
   265  		err = e.Err
   266  	}
   267  }
   268  
   269  // ErrAlreadyHandled is an error that is already handled by
   270  // a kpt command and nothing needs to be done by the global
   271  // error handler except to return a non-zero exit code.
   272  var ErrAlreadyHandled = fmt.Errorf("already handled error")