drone/gitrpc/errors.go

217 lines
5.2 KiB
Go

// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gitrpc
import (
"errors"
"fmt"
"github.com/harness/gitness/gitrpc/rpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var (
ErrNoParamsProvided = ErrInvalidArgumentf("params not provided")
)
const (
conflictFilesKey = "conflict_files"
pathKey = "path"
)
type Status string
const (
StatusConflict Status = "conflict"
StatusInternal Status = "internal"
StatusInvalidArgument Status = "invalid"
StatusNotFound Status = "not_found"
StatusPathNotFound Status = "path_not_found"
StatusNotImplemented Status = "not_implemented"
StatusUnauthorized Status = "unauthorized"
StatusFailed Status = "failed"
StatusPreconditionFailed Status = "precondition_failed"
StatusNotMergeable Status = "not_mergeable"
StatusAborted Status = "aborted"
)
type Error struct {
// Machine-readable status code.
Status Status
// Human-readable error message.
Message string
// Details
Details map[string]any
}
// Error implements the error interface.
func (e *Error) Error() string {
return e.Message
}
// ErrorStatus unwraps an gitrpc error and returns its code.
// Non-application errors always return StatusInternal.
func ErrorStatus(err error) Status {
var (
e *Error
)
if err == nil {
return ""
}
if errors.As(err, &e) {
return e.Status
}
return StatusInternal
}
// ErrorMessage unwraps an gitrpc error and returns its message.
// Non-gitrpc errors always return "Internal error".
func ErrorMessage(err error) string {
var (
e *Error
)
if err == nil {
return ""
}
if errors.As(err, &e) {
return e.Message
}
return "Internal error."
}
// ErrorDetails unwraps an gitrpc error and returns its details.
// Non-gitrpc errors always return nil.
func ErrorDetails(err error) map[string]any {
var (
e *Error
)
if err == nil {
return nil
}
if errors.As(err, &e) {
return e.Details
}
return nil
}
// NewError is a factory function to return an Error with a given status and message.
func NewError(code Status, message string) *Error {
return &Error{
Status: code,
Message: message,
}
}
// NewError is a factory function to return an Error with a given status, message and details.
func NewErrorWithDetails(code Status, message string, details map[string]any) *Error {
err := NewError(code, message)
err.Details = details
return err
}
// Errorf is a helper function to return an Error with a given status and formatted message.
func Errorf(code Status, format string, args ...interface{}) *Error {
return &Error{
Status: code,
Message: fmt.Sprintf(format, args...),
}
}
// ErrInvalidArgumentf is a helper function to return an invalid argument Error.
func ErrInvalidArgumentf(format string, args ...interface{}) *Error {
return Errorf(StatusInvalidArgument, format, args...)
}
func processRPCErrorf(err error, format string, args ...interface{}) error {
if errors.Is(err, &Error{}) {
return err
}
// create fallback error returned if we can't map it
fallbackMsg := fmt.Sprintf(format, args...)
fallbackErr := NewError(StatusInternal, fallbackMsg)
// ensure it's an rpc error
st, ok := status.FromError(err)
if !ok {
return fallbackErr
}
msg := st.Message()
switch {
case st.Code() == codes.AlreadyExists:
return NewError(StatusConflict, msg)
case st.Code() == codes.NotFound:
code := StatusNotFound
details := make(map[string]any)
for _, detail := range st.Details() {
switch t := detail.(type) {
case *rpc.PathNotFoundError:
code = StatusPathNotFound
details[pathKey] = t.Path
default:
}
}
if len(details) > 0 {
return NewErrorWithDetails(code, msg, details)
}
return NewError(code, msg)
case st.Code() == codes.InvalidArgument:
return NewError(StatusInvalidArgument, msg)
case st.Code() == codes.FailedPrecondition:
code := StatusPreconditionFailed
details := make(map[string]any)
for _, detail := range st.Details() {
switch t := detail.(type) {
case *rpc.MergeConflictError:
details[conflictFilesKey] = t.ConflictingFiles
code = StatusNotMergeable
default:
}
}
if len(details) > 0 {
return NewErrorWithDetails(code, msg, details)
}
return NewError(code, msg)
default:
return fallbackErr
}
}
func AsConflictFilesError(err error) (files []string) {
details := ErrorDetails(err)
object, ok := details[conflictFilesKey]
if ok {
files, _ = object.([]string)
}
return
}
// AsPathNotFoundError returns the path that wasn't found in case that's the error.
func AsPathNotFoundError(err error) (path string) {
details := ErrorDetails(err)
object, ok := details[pathKey]
if ok {
path = object.(string)
}
return
}