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")