diff --git a/CHANGELOG.md b/CHANGELOG.md index 48d2affe4..2e3fba5cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - (Bugfix) Enable Platform Operator on EE Chart - (Feature) Improve GRPC JSON Handling - (Bugfix) Fix Operator Pod Resources +- (Feature) Improve Session & Route Handling ## [1.3.0](https://github.com/arangodb/kube-arangodb/tree/1.3.0) (2025-08-01) - (Feature) (Platform) Storage Debug diff --git a/integrations/envoy/auth/v3/impl/auth_custom/openid/impl.go b/integrations/envoy/auth/v3/impl/auth_custom/openid/impl.go index f16f7cd23..9ac192614 100644 --- a/integrations/envoy/auth/v3/impl/auth_custom/openid/impl.go +++ b/integrations/envoy/auth/v3/impl/auth_custom/openid/impl.go @@ -329,6 +329,10 @@ func (i *impl) handleOpenIDAuthentication(ctx context.Context, request *pbEnvoyA }, }) + if _, err := i.session.Invalidate(ctx, cookie.Value); err != nil { + return err + } + continue } @@ -355,20 +359,31 @@ func (i *impl) handleOpenIDAuthentication(ctx context.Context, request *pbEnvoyA continue } - session, cookie, err := i.handleOpenIDRefresh(ctx, cfg, ocfg, session) + session, ok, _, err := i.session.Refresh(ctx, cookie.Value) if err != nil { return err } - current.User = session.AsResponse() + if ok { + newSession, newCookie, err := i.handleOpenIDRefresh(ctx, cfg, ocfg, session) + if err != nil { + return err + } - if cookie != nil { - current.ResponseHeaders = append(current.ResponseHeaders, &pbEnvoyCoreV3.HeaderValueOption{ - Header: &pbEnvoyCoreV3.HeaderValue{ - Key: "Set-Cookie", - Value: cookie.String(), - }, - }) + current.User = newSession.AsResponse() + + if newCookie != nil { + current.ResponseHeaders = append(current.ResponseHeaders, &pbEnvoyCoreV3.HeaderValueOption{ + Header: &pbEnvoyCoreV3.HeaderValue{ + Key: "Set-Cookie", + Value: newCookie.String(), + }, + }) + } + + if _, err := i.session.Invalidate(ctx, cookie.Value); err != nil { + return err + } } continue diff --git a/integrations/envoy/auth/v3/impl/session/session.go b/integrations/envoy/auth/v3/impl/session/session.go index 394246f8d..4ebd827f4 100644 --- a/integrations/envoy/auth/v3/impl/session/session.go +++ b/integrations/envoy/auth/v3/impl/session/session.go @@ -38,7 +38,7 @@ import ( func NewManager[T any](ctx context.Context, t Type, client cache.Object[arangodb.Collection]) Manager[T] { return manager[T]{ t: t, - cache: cache.NewRemoteCache[*session](client), + cache: cache.NewRemoteCacheWithTTL[*session](client, 5*time.Second), } } @@ -47,6 +47,8 @@ type Manager[T any] interface { Get(ctx context.Context, key string) (T, bool, time.Duration, error) + Refresh(ctx context.Context, key string) (T, bool, time.Duration, error) + Invalidate(ctx context.Context, key string) (bool, error) } @@ -55,6 +57,12 @@ type manager[T any] struct { cache cache.RemoteCache[*session] } +func (m manager[T]) Refresh(ctx context.Context, key string) (T, bool, time.Duration, error) { + m.cache.Invalidate(ctx, key) + + return m.Get(ctx, key) +} + func (m manager[T]) Put(ctx context.Context, expires time.Time, obj T) (string, error) { data, err := sharedApi.NewAny(obj) if err != nil { diff --git a/integrations/shared/v1/definition/errors.pb.go b/integrations/shared/v1/definition/errors.pb.go new file mode 100644 index 000000000..a0dfae7a2 --- /dev/null +++ b/integrations/shared/v1/definition/errors.pb.go @@ -0,0 +1,244 @@ +// +// DISCLAIMER +// +// Copyright 2024 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.26.0 +// protoc v3.21.1 +// source: integrations/shared/v1/definition/errors.proto + +package definition + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Path Error Details +type PathError struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Path + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + // Message + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` +} + +func (x *PathError) Reset() { + *x = PathError{} + if protoimpl.UnsafeEnabled { + mi := &file_integrations_shared_v1_definition_errors_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PathError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PathError) ProtoMessage() {} + +func (x *PathError) ProtoReflect() protoreflect.Message { + mi := &file_integrations_shared_v1_definition_errors_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PathError.ProtoReflect.Descriptor instead. +func (*PathError) Descriptor() ([]byte, []int) { + return file_integrations_shared_v1_definition_errors_proto_rawDescGZIP(), []int{0} +} + +func (x *PathError) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *PathError) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +// Path Error Details +type Error struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Message + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` +} + +func (x *Error) Reset() { + *x = Error{} + if protoimpl.UnsafeEnabled { + mi := &file_integrations_shared_v1_definition_errors_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Error) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Error) ProtoMessage() {} + +func (x *Error) ProtoReflect() protoreflect.Message { + mi := &file_integrations_shared_v1_definition_errors_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Error.ProtoReflect.Descriptor instead. +func (*Error) Descriptor() ([]byte, []int) { + return file_integrations_shared_v1_definition_errors_proto_rawDescGZIP(), []int{1} +} + +func (x *Error) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +var File_integrations_shared_v1_definition_errors_proto protoreflect.FileDescriptor + +var file_integrations_shared_v1_definition_errors_proto_rawDesc = []byte{ + 0x0a, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x73, + 0x68, 0x61, 0x72, 0x65, 0x64, 0x2f, 0x76, 0x31, 0x2f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x2f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x12, 0x06, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x22, 0x39, 0x0a, 0x09, 0x50, 0x61, 0x74, 0x68, + 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x22, 0x21, 0x0a, 0x05, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x18, 0x0a, 0x07, + 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x42, 0x45, 0x5a, 0x43, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x72, 0x61, 0x6e, 0x67, 0x6f, 0x64, 0x62, 0x2f, 0x6b, 0x75, + 0x62, 0x65, 0x2d, 0x61, 0x72, 0x61, 0x6e, 0x67, 0x6f, 0x64, 0x62, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x2f, + 0x76, 0x31, 0x2f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_integrations_shared_v1_definition_errors_proto_rawDescOnce sync.Once + file_integrations_shared_v1_definition_errors_proto_rawDescData = file_integrations_shared_v1_definition_errors_proto_rawDesc +) + +func file_integrations_shared_v1_definition_errors_proto_rawDescGZIP() []byte { + file_integrations_shared_v1_definition_errors_proto_rawDescOnce.Do(func() { + file_integrations_shared_v1_definition_errors_proto_rawDescData = protoimpl.X.CompressGZIP(file_integrations_shared_v1_definition_errors_proto_rawDescData) + }) + return file_integrations_shared_v1_definition_errors_proto_rawDescData +} + +var file_integrations_shared_v1_definition_errors_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_integrations_shared_v1_definition_errors_proto_goTypes = []interface{}{ + (*PathError)(nil), // 0: shared.PathError + (*Error)(nil), // 1: shared.Error +} +var file_integrations_shared_v1_definition_errors_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_integrations_shared_v1_definition_errors_proto_init() } +func file_integrations_shared_v1_definition_errors_proto_init() { + if File_integrations_shared_v1_definition_errors_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_integrations_shared_v1_definition_errors_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PathError); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_integrations_shared_v1_definition_errors_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Error); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_integrations_shared_v1_definition_errors_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_integrations_shared_v1_definition_errors_proto_goTypes, + DependencyIndexes: file_integrations_shared_v1_definition_errors_proto_depIdxs, + MessageInfos: file_integrations_shared_v1_definition_errors_proto_msgTypes, + }.Build() + File_integrations_shared_v1_definition_errors_proto = out.File + file_integrations_shared_v1_definition_errors_proto_rawDesc = nil + file_integrations_shared_v1_definition_errors_proto_goTypes = nil + file_integrations_shared_v1_definition_errors_proto_depIdxs = nil +} diff --git a/integrations/shared/v1/definition/errors.proto b/integrations/shared/v1/definition/errors.proto new file mode 100644 index 000000000..25fbc4c03 --- /dev/null +++ b/integrations/shared/v1/definition/errors.proto @@ -0,0 +1,40 @@ +// +// DISCLAIMER +// +// Copyright 2024 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +syntax = "proto3"; + +package shared; + +option go_package = "github.com/arangodb/kube-arangodb/integrations/shared/v1/definition"; + +// Path Error Details +message PathError { + // Path + string path = 1; + // Message + string message = 2; +} + +// Path Error Details +message Error { + // Message + string message = 1; +} + diff --git a/pkg/apis/shared/errors.go b/pkg/apis/shared/errors.go index e4bcae2c9..00f980fe1 100644 --- a/pkg/apis/shared/errors.go +++ b/pkg/apis/shared/errors.go @@ -24,6 +24,7 @@ import ( "fmt" "io" + pbSharedV1 "github.com/arangodb/kube-arangodb/integrations/shared/v1/definition" "github.com/arangodb/kube-arangodb/pkg/util/errors" ) @@ -32,6 +33,13 @@ type ResourceError struct { Err error } +func (p ResourceError) AsGRPCError() *pbSharedV1.PathError { + return &pbSharedV1.PathError{ + Path: p.Prefix, + Message: p.Err.Error(), + } +} + // Error return string representation of error func (p ResourceError) Error() string { return fmt.Sprintf("%s: %s", p.Prefix, p.Err.Error()) diff --git a/pkg/apis/shared/validate.go b/pkg/apis/shared/validate.go index b5b20c156..46da40958 100644 --- a/pkg/apis/shared/validate.go +++ b/pkg/apis/shared/validate.go @@ -259,11 +259,11 @@ func ValidateInterfaceMap[T ValidateInterface](in map[string]T) error { } // ValidateMap validates all elements on the list -func ValidateMap[T any](in map[string]T, validator func(string, T) error, checks ...func(in map[string]T) error) error { +func ValidateMap[K comparable, T any](in map[K]T, validator func(K, T) error, checks ...func(in map[K]T) error) error { errors := make([]error, 0, len(in)+len(checks)) for id := range in { - if err := PrefixResourceError(fmt.Sprintf("`%s`", id), validator(id, in[id])); err != nil { + if err := PrefixResourceError(fmt.Sprintf("`%v`", id), validator(id, in[id])); err != nil { errors = append(errors, err) } } diff --git a/pkg/deployment/client/inventory.go b/pkg/deployment/client/inventory.go index 31ff5ff6e..c03a068b6 100644 --- a/pkg/deployment/client/inventory.go +++ b/pkg/deployment/client/inventory.go @@ -36,7 +36,7 @@ type InventoryConfiguration struct { } func (c *client) Inventory(ctx context.Context) (*Inventory, error) { - req, err := c.c.NewRequest(goHttp.MethodGet, utilConstants.EnvoyInventoryConfigDestination) + req, err := c.c.NewRequest(goHttp.MethodGet, utilConstants.EnvoyInventoryConfigDestination.String()) if err != nil { return nil, err } diff --git a/pkg/deployment/resources/config_map_gateway.go b/pkg/deployment/resources/config_map_gateway.go index b33266adb..a0128640f 100644 --- a/pkg/deployment/resources/config_map_gateway.go +++ b/pkg/deployment/resources/config_map_gateway.go @@ -313,6 +313,11 @@ func (r *Resources) renderGatewayConfig(cachedStatus inspectorInterface.Inspecto log.Warn("ArangoRoute Route Path not defined") return nil } + var key = utilConstants.EnvoyDestination(target.Route.Path) + if key.IsReserved() { + log.Warn("ArangoRoute Route Path `%s` is reserved", key) + return nil + } var dest gateway.ConfigDestination if destinations := target.Destinations; len(destinations) > 0 { for _, destination := range destinations { @@ -353,7 +358,10 @@ func (r *Resources) renderGatewayConfig(cachedStatus inspectorInterface.Inspecto dest.ResponseHeaders = map[string]string{ utilConstants.EnvoyRouteHeader: at.GetName(), } - cfg.Destinations[target.Route.Path] = dest + if err := cfg.Destinations.Append(key, dest); err != nil { + log.Err(err).Warn("Unable to add destination `%s`", key) + return nil + } } return nil diff --git a/pkg/deployment/resources/gateway/gateway_config.go b/pkg/deployment/resources/gateway/gateway_config.go index cc7d22936..d2de1d1ed 100644 --- a/pkg/deployment/resources/gateway/gateway_config.go +++ b/pkg/deployment/resources/gateway/gateway_config.go @@ -222,7 +222,7 @@ func (c Config) RenderClusters() ([]*pbEnvoyClusterV3.Cluster, error) { } for k, v := range c.Destinations { - name := fmt.Sprintf("cluster_%s", util.SHA256FromString(k)) + name := fmt.Sprintf("cluster_%s", util.SHA256FromString(k.String())) c, err := v.RenderCluster(name) if err != nil { return nil, err @@ -251,8 +251,8 @@ func (c Config) RenderRoutes() ([]*pbEnvoyRouteV3.Route, error) { } for k, v := range c.Destinations { - name := fmt.Sprintf("cluster_%s", util.SHA256FromString(k)) - c, err := v.RenderRoute(name, k) + name := fmt.Sprintf("cluster_%s", util.SHA256FromString(k.String())) + c, err := v.RenderRoute(name, k.String()) if err != nil { return nil, err } diff --git a/pkg/deployment/resources/gateway/gateway_config_destination.go b/pkg/deployment/resources/gateway/gateway_config_destination.go index d552b9d9d..4c5e5052c 100644 --- a/pkg/deployment/resources/gateway/gateway_config_destination.go +++ b/pkg/deployment/resources/gateway/gateway_config_destination.go @@ -36,19 +36,37 @@ import ( "github.com/arangodb/kube-arangodb/pkg/util/errors" ) -type ConfigDestinations map[string]ConfigDestination +type ConfigDestinations map[utilConstants.EnvoyDestination]ConfigDestination + +func (c *ConfigDestinations) Append(key utilConstants.EnvoyDestination, value ConfigDestination) error { + if c == nil { + return errors.Errorf("Unable to assign to nil map") + } + + v := *c + + if _, ok := v[key]; ok { + return errors.Errorf("Destination `%s` already exists", key) + } + + v[key] = value + + *c = v + + return nil +} func (c ConfigDestinations) Validate() error { if len(c) == 0 { return nil } return shared.WithErrors( - shared.ValidateMap(c, func(k string, destination ConfigDestination) error { + shared.ValidateMap(c, func(k utilConstants.EnvoyDestination, destination ConfigDestination) error { var errs []error if k == "/" { errs = append(errs, errors.Errorf("Route for `/` is reserved")) } - if err := shared.ValidateAPIPath(k); err != nil { + if err := shared.ValidateAPIPath(k.String()); err != nil { errs = append(errs, err) } if err := destination.Validate(); err != nil { diff --git a/pkg/util/constants/envoy.go b/pkg/util/constants/envoy.go index f554036f6..6f8db135d 100644 --- a/pkg/util/constants/envoy.go +++ b/pkg/util/constants/envoy.go @@ -20,13 +20,15 @@ package constants +type EnvoyDestination string + const ( EnvoyRouteHeader = "arangodb-platform-route" - EnvoyInventoryConfigDestination = "/_inventory" - EnvoyIdentityDestination = "/_identity" - EnvoyLoginDestination = "/_login" - EnvoyLogoutDestination = "/_logout" + EnvoyInventoryConfigDestination EnvoyDestination = "/_inventory" + EnvoyIdentityDestination EnvoyDestination = "/_identity" + EnvoyLoginDestination EnvoyDestination = "/_login" + EnvoyLogoutDestination EnvoyDestination = "/_logout" EnvoyIntegrationSidecarFilterName = "envoy.filters.http.ext_authz" @@ -34,3 +36,16 @@ const ( EnvoyIntegrationSidecarClusterHTTP = "integration_sidecar_http" ) + +func (d EnvoyDestination) IsReserved() bool { + switch d { + case EnvoyInventoryConfigDestination, EnvoyIdentityDestination, EnvoyLoginDestination, EnvoyLogoutDestination: + return true + } + + return false +} + +func (d EnvoyDestination) String() string { + return string(d) +} diff --git a/pkg/util/grpc/errors.go b/pkg/util/grpc/errors.go new file mode 100644 index 000000000..a66a0ddca --- /dev/null +++ b/pkg/util/grpc/errors.go @@ -0,0 +1,97 @@ +// +// DISCLAIMER +// +// Copyright 2025 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package grpc + +import ( + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/protoadapt" + + pbSharedV1 "github.com/arangodb/kube-arangodb/integrations/shared/v1/definition" + "github.com/arangodb/kube-arangodb/pkg/util/errors" +) + +func NewGRPCError(code codes.Code, msg string, args ...interface{}) GRPCError { + return &grpcError{ + err: status.Newf(code, msg, args...), + } +} + +type GRPCError interface { + With(err ...error) GRPCError + Err() error +} + +type grpcError struct { + err *status.Status +} + +func (g grpcError) With(errs ...error) GRPCError { + if g.err.Code() == codes.OK { + return g + } + + e := g.err + + for _, err := range errs { + if v, ok := err.(errors.Array); ok { + if len(v) > 0 { + p := make([]protoadapt.MessageV1, len(v)) + + for i, n := range v { + p[i] = AsGRPCMessage(n) + } + + if q, err := g.err.WithDetails(p...); err == nil { + e = q + } + } + } else { + if q, err := e.WithDetails(AsGRPCMessage(err)); err == nil { + e = q + } + } + } + + return grpcError{ + err: e, + } +} + +func (g grpcError) Err() error { + return g.err.Err() +} + +type Interface interface { + AsGRPCError() protoadapt.MessageV1 +} + +func AsGRPCMessage(err error) protoadapt.MessageV1 { + if err == nil { + return &pbSharedV1.Error{Message: "unknown error"} + } + + if v, ok := err.(Interface); ok { + return v.AsGRPCError() + } + + return &pbSharedV1.Error{Message: err.Error()} +}