diff --git a/go.mod b/go.mod index 8b121d9b8..280d0fe54 100644 --- a/go.mod +++ b/go.mod @@ -11,11 +11,14 @@ require ( github.com/krolaw/dhcp4 v0.0.0-20190909130307-a50d88189771 github.com/pin/tftp v2.1.0+incompatible golang.org/x/crypto v0.30.0 - golang.org/x/net v0.21.0 + golang.org/x/net v0.25.0 + golang.org/x/sys v0.28.0 + google.golang.org/grpc v1.65.0 ) require ( github.com/jmespath/go-jmespath v0.4.0 // indirect - golang.org/x/sys v0.28.0 // indirect golang.org/x/term v0.27.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect + google.golang.org/protobuf v1.34.1 // indirect ) diff --git a/go.sum b/go.sum index 2be545db9..246c9e431 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -22,30 +24,29 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b h1:huxqepDufQpLLIRXiVkTvnxrzJlpwmIWAObmcCcUFr0= -golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20221004154528-8021a29435af h1:wv66FM3rLZGPdxpYL+ApnDe2HzHcTFta3z5nsc13wI4= -golang.org/x/net v0.0.0-20221004154528-8021a29435af/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/lib/errors/types.go b/lib/errors/types.go new file mode 100644 index 000000000..8e9cd8342 --- /dev/null +++ b/lib/errors/types.go @@ -0,0 +1,200 @@ +package errors + +import "google.golang.org/grpc/codes" + +type NotFoundError struct { + Resource string + ID string +} + +func (e *NotFoundError) Error() string { + if e.ID != "" { + return e.Resource + " " + e.ID + " not found" + } + return e.Resource + " not found" +} + +func (e *NotFoundError) GrpcCode() codes.Code { return codes.NotFound } + +func NewNotFoundError(resource, id string) *NotFoundError { + return &NotFoundError{Resource: resource, ID: id} +} + +type PermissionDeniedError struct { + Resource string + Action string + Reason string +} + +func (e *PermissionDeniedError) Error() string { + if e.Reason != "" { + return "permission denied: " + e.Action + " on " + e.Resource + ": " + e.Reason + } + return "permission denied: " + e.Action + " on " + e.Resource +} + +func (e *PermissionDeniedError) GrpcCode() codes.Code { return codes.PermissionDenied } + +func NewPermissionDeniedError(resource, action, reason string) *PermissionDeniedError { + return &PermissionDeniedError{Resource: resource, Action: action, Reason: reason} +} + +type UnauthenticatedError struct { + Message string +} + +func (e *UnauthenticatedError) Error() string { + if e.Message != "" { + return "unauthenticated: " + e.Message + } + return "unauthenticated" +} + +func (e *UnauthenticatedError) GrpcCode() codes.Code { return codes.Unauthenticated } + +func NewUnauthenticatedError(message string) *UnauthenticatedError { + return &UnauthenticatedError{Message: message} +} + +type AlreadyExistsError struct { + Resource string + ID string +} + +func (e *AlreadyExistsError) Error() string { + if e.ID != "" { + return e.Resource + " " + e.ID + " already exists" + } + return e.Resource + " already exists" +} + +func (e *AlreadyExistsError) GrpcCode() codes.Code { return codes.AlreadyExists } + +func NewAlreadyExistsError(resource, id string) *AlreadyExistsError { + return &AlreadyExistsError{Resource: resource, ID: id} +} + +type InvalidArgumentError struct { + Argument string + Reason string +} + +func (e *InvalidArgumentError) Error() string { + if e.Reason != "" { + return "invalid argument " + e.Argument + ": " + e.Reason + } + return "invalid argument: " + e.Argument +} + +func (e *InvalidArgumentError) GrpcCode() codes.Code { return codes.InvalidArgument } + +func NewInvalidArgumentError(argument, reason string) *InvalidArgumentError { + return &InvalidArgumentError{Argument: argument, Reason: reason} +} + +type FailedPreconditionError struct { + Resource string + State string + Reason string +} + +func (e *FailedPreconditionError) Error() string { + if e.Reason != "" { + return e.Resource + " " + e.State + ": " + e.Reason + } + return e.Resource + " " + e.State +} + +func (e *FailedPreconditionError) GrpcCode() codes.Code { return codes.FailedPrecondition } + +func NewFailedPreconditionError(resource, state, reason string) *FailedPreconditionError { + return &FailedPreconditionError{Resource: resource, State: state, Reason: reason} +} + +type UnavailableError struct { + Service string + Reason string +} + +func (e *UnavailableError) Error() string { + if e.Reason != "" { + return e.Service + " unavailable: " + e.Reason + } + return e.Service + " unavailable" +} + +func (e *UnavailableError) GrpcCode() codes.Code { return codes.Unavailable } + +func NewUnavailableError(service, reason string) *UnavailableError { + return &UnavailableError{Service: service, Reason: reason} +} + +type DeadlineExceededError struct { + Operation string + Timeout string +} + +func (e *DeadlineExceededError) Error() string { + if e.Timeout != "" { + return e.Operation + " exceeded deadline of " + e.Timeout + } + return e.Operation + " exceeded deadline" +} + +func (e *DeadlineExceededError) GrpcCode() codes.Code { return codes.DeadlineExceeded } + +func NewDeadlineExceededError(operation, timeout string) *DeadlineExceededError { + return &DeadlineExceededError{Operation: operation, Timeout: timeout} +} + +type ResourceExhaustedError struct { + Resource string + Reason string +} + +func (e *ResourceExhaustedError) Error() string { + if e.Reason != "" { + return e.Resource + " exhausted: " + e.Reason + } + return e.Resource + " exhausted" +} + +func (e *ResourceExhaustedError) GrpcCode() codes.Code { return codes.ResourceExhausted } + +func NewResourceExhaustedError(resource, reason string) *ResourceExhaustedError { + return &ResourceExhaustedError{Resource: resource, Reason: reason} +} + +type InternalError struct { + Message string +} + +func (e *InternalError) Error() string { + if e.Message != "" { + return "internal error: " + e.Message + } + return "internal error" +} + +func (e *InternalError) GrpcCode() codes.Code { return codes.Internal } + +func NewInternalError(message string) *InternalError { + return &InternalError{Message: message} +} + +type UnimplementedError struct { + Operation string +} + +func (e *UnimplementedError) Error() string { + if e.Operation != "" { + return e.Operation + " not implemented" + } + return "not implemented" +} + +func (e *UnimplementedError) GrpcCode() codes.Code { return codes.Unimplemented } + +func NewUnimplementedError(operation string) *UnimplementedError { + return &UnimplementedError{Operation: operation} +} diff --git a/lib/grpc/errors.go b/lib/grpc/errors.go new file mode 100644 index 000000000..de5d7c4b7 --- /dev/null +++ b/lib/grpc/errors.go @@ -0,0 +1,85 @@ +package grpc + +import ( + "errors" + "strings" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// CodedError is implemented by errors that provide a gRPC status code. +type CodedError interface { + GrpcCode() codes.Code +} + +var prefixMappings = []struct { + prefix string + code codes.Code +}{ + {"unauthenticated:", codes.Unauthenticated}, + {"permission denied:", codes.PermissionDenied}, + {"precondition failed:", codes.FailedPrecondition}, + {"not found:", codes.NotFound}, + {"already exists:", codes.AlreadyExists}, + {"invalid argument:", codes.InvalidArgument}, + {"unavailable:", codes.Unavailable}, + {"deadline exceeded:", codes.DeadlineExceeded}, + {"resource exhausted:", codes.ResourceExhausted}, + {"internal:", codes.Internal}, +} + +var patternMappings = []struct { + pattern string + code codes.Code +}{ + // Unauthenticated + {"no authentication", codes.Unauthenticated}, + {"unauthenticated", codes.Unauthenticated}, + {"not authenticated", codes.Unauthenticated}, + {"authentication required", codes.Unauthenticated}, + // PermissionDenied + {"permission denied", codes.PermissionDenied}, + {"access denied", codes.PermissionDenied}, + {"forbidden", codes.PermissionDenied}, + {"no access", codes.PermissionDenied}, + // NotFound + {"not found", codes.NotFound}, + {"does not exist", codes.NotFound}, + // AlreadyExists + {"already exists", codes.AlreadyExists}, + {"duplicate", codes.AlreadyExists}, + // InvalidArgument + {"invalid", codes.InvalidArgument}, + {"malformed", codes.InvalidArgument}, + // Unavailable + {"unavailable", codes.Unavailable}, + {"connection refused", codes.Unavailable}, + // DeadlineExceeded + {"timeout", codes.DeadlineExceeded}, + {"timed out", codes.DeadlineExceeded}, +} + +// ErrorToStatus converts errors to gRPC status codes. +func ErrorToStatus(err error) error { + if err == nil { + return nil + } + var codedErr CodedError + if errors.As(err, &codedErr) { + return status.Error(codedErr.GrpcCode(), err.Error()) + } + msg := err.Error() + lower := strings.ToLower(msg) + for _, m := range prefixMappings { + if strings.HasPrefix(lower, m.prefix) { + return status.Error(m.code, msg) + } + } + for _, m := range patternMappings { + if strings.Contains(lower, m.pattern) { + return status.Error(m.code, msg) + } + } + return status.Error(codes.Internal, msg) +}