From 079c63af4f99972671b1a4d89229c897cab1c06b Mon Sep 17 00:00:00 2001 From: James-Milligan Date: Mon, 5 Sep 2022 17:05:40 +0100 Subject: [PATCH 01/21] initial commit - tested with rest, need to build remaining clients Signed-off-by: James-Milligan --- go.mod | 4 +- go.sum | 4 ++ pkg/runtime/from_config.go | 2 + pkg/service/connect_service.go | 115 +++++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 pkg/service/connect_service.go diff --git a/go.mod b/go.mod index 95c3cd047..d1034d6b2 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/open-feature/flagd go 1.18 require ( + github.com/bufbuild/connect-go v0.4.0 github.com/deepmap/oapi-codegen v1.11.0 github.com/diegoholiveira/jsonlogic/v3 v3.2.3 github.com/dimiro1/banner v1.1.0 @@ -20,7 +21,9 @@ require ( github.com/stretchr/testify v1.7.4 github.com/xeipuuv/gojsonschema v1.2.0 github.com/zeebo/xxh3 v1.0.2 + go.buf.build/bufbuild/connect-go/open-feature/flagd v1.7.2 go.buf.build/open-feature/flagd-server/open-feature/flagd v1.1.2 + golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f google.golang.org/grpc v1.48.0 google.golang.org/protobuf v1.28.1 @@ -51,7 +54,6 @@ require ( github.com/subosito/gotenv v1.3.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c // indirect golang.org/x/text v0.3.7 // indirect google.golang.org/genproto v0.0.0-20220728213248-dd149ef739b9 // indirect diff --git a/go.sum b/go.sum index d9f466331..bca494cca 100644 --- a/go.sum +++ b/go.sum @@ -61,6 +61,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/bufbuild/connect-go v0.4.0 h1:fIMyUYG8mXSTH+nnlOx9KmRUf3mBF0R2uKK+BQBoOHE= +github.com/bufbuild/connect-go v0.4.0/go.mod h1:ZEtBnQ7J/m7bvWOW+H8T/+hKQCzPVfhhhICuvtcnjlI= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -356,6 +358,8 @@ github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +go.buf.build/bufbuild/connect-go/open-feature/flagd v1.7.2 h1:IzQZ2lGT44mLOB8roKxQHEOZShMRZNH49cRBZGavaxw= +go.buf.build/bufbuild/connect-go/open-feature/flagd v1.7.2/go.mod h1:VpawqcW+HY3faAQvWpTJQOdTGT+w8pCWVif9C4Jd35M= go.buf.build/open-feature/flagd-server/open-feature/flagd v1.1.2 h1:GbrVEHuAp/3a6JpfoV8cusP4/CveInar997DCCbXSAg= go.buf.build/open-feature/flagd-server/open-feature/flagd v1.1.2/go.mod h1:1gOZbwcemK1/404A8u4kXmzIn4DIIT1xIe8JuowznT8= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= diff --git a/pkg/runtime/from_config.go b/pkg/runtime/from_config.go index f8f37b7ce..7f59f7175 100644 --- a/pkg/runtime/from_config.go +++ b/pkg/runtime/from_config.go @@ -62,6 +62,8 @@ func (r *Runtime) setServiceFromConfig() error { "component": "service", }), } + case "connect": + r.Service = &service.ConnectService{} default: return errors.New("no service-provider set") } diff --git a/pkg/service/connect_service.go b/pkg/service/connect_service.go new file mode 100644 index 000000000..2320bd524 --- /dev/null +++ b/pkg/service/connect_service.go @@ -0,0 +1,115 @@ +package service + +import ( + "context" + "net/http" + + "github.com/bufbuild/connect-go" + "github.com/open-feature/flagd/pkg/eval" + log "github.com/sirupsen/logrus" + schemaV1 "go.buf.build/bufbuild/connect-go/open-feature/flagd/schema/v1" + schemaConnectV1 "go.buf.build/bufbuild/connect-go/open-feature/flagd/schema/v1/schemav1connect" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" + "google.golang.org/protobuf/types/known/structpb" +) + +type ConnectService struct { + Eval eval.IEvaluator +} + +func (s *ConnectService) Serve(ctx context.Context, eval eval.IEvaluator) error { + s.Eval = eval + mux := http.NewServeMux() + path, handler := schemaConnectV1.NewServiceHandler(s) + mux.Handle(path, handler) + return http.ListenAndServe( + "localhost:8080", + // Use h2c so we can serve HTTP/2 without TLS. + h2c.NewHandler(mux, &http2.Server{}), + ) +} + +func (s *ConnectService) ResolveBoolean( + ctx context.Context, + req *connect.Request[schemaV1.ResolveBooleanRequest], +) (*connect.Response[schemaV1.ResolveBooleanResponse], error) { + res := connect.NewResponse(&schemaV1.ResolveBooleanResponse{}) + result, variant, reason, err := s.Eval.ResolveBooleanValue(req.Msg.GetFlagKey(), req.Msg.GetContext()) + if err != nil { + log.Error(err) + return res, err + } + res.Msg.Reason = reason + res.Msg.Value = result + res.Msg.Variant = variant + return res, nil +} + +func (s *ConnectService) ResolveString( + ctx context.Context, + req *connect.Request[schemaV1.ResolveStringRequest], +) (*connect.Response[schemaV1.ResolveStringResponse], error) { + res := connect.NewResponse(&schemaV1.ResolveStringResponse{}) + result, variant, reason, err := s.Eval.ResolveStringValue(req.Msg.GetFlagKey(), req.Msg.GetContext()) + if err != nil { + log.Error(err) + return res, err + } + res.Msg.Reason = reason + res.Msg.Value = result + res.Msg.Variant = variant + return res, nil +} + +func (s *ConnectService) ResolveInt( + ctx context.Context, + req *connect.Request[schemaV1.ResolveIntRequest], +) (*connect.Response[schemaV1.ResolveIntResponse], error) { + res := connect.NewResponse(&schemaV1.ResolveIntResponse{}) + result, variant, reason, err := s.Eval.ResolveIntValue(req.Msg.GetFlagKey(), req.Msg.GetContext()) + if err != nil { + log.Error(err) + return res, err + } + res.Msg.Reason = reason + res.Msg.Value = result + res.Msg.Variant = variant + return res, nil +} + +func (s *ConnectService) ResolveFloat( + ctx context.Context, + req *connect.Request[schemaV1.ResolveFloatRequest], +) (*connect.Response[schemaV1.ResolveFloatResponse], error) { + res := connect.NewResponse(&schemaV1.ResolveFloatResponse{}) + result, variant, reason, err := s.Eval.ResolveFloatValue(req.Msg.GetFlagKey(), req.Msg.GetContext()) + if err != nil { + log.Error(err) + return res, err + } + res.Msg.Reason = reason + res.Msg.Value = result + res.Msg.Variant = variant + return res, nil +} + +func (s *ConnectService) ResolveObject( + ctx context.Context, + req *connect.Request[schemaV1.ResolveObjectRequest], +) (*connect.Response[schemaV1.ResolveObjectResponse], error) { + res := connect.NewResponse(&schemaV1.ResolveObjectResponse{}) + result, variant, reason, err := s.Eval.ResolveObjectValue(req.Msg.GetFlagKey(), req.Msg.GetContext()) + if err != nil { + log.Error(err) + return res, err + } + val, err := structpb.NewStruct(result) + if err != nil { + return res, err + } + res.Msg.Reason = reason + res.Msg.Value = val + res.Msg.Variant = variant + return res, nil +} From a0e9f4c38009b6cacb7601aa7cd16d59aea254ba Mon Sep 17 00:00:00 2001 From: James-Milligan Date: Wed, 7 Sep 2022 11:11:59 +0100 Subject: [PATCH 02/21] added TLS support Signed-off-by: James-Milligan --- go.mod | 1 + go.sum | 2 + pkg/runtime/from_config.go | 9 ++- pkg/service/connect_service.go | 117 +++++++++++++++++++++++++++++---- 4 files changed, 115 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index d1034d6b2..5e725534e 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/mattn/go-colorable v0.1.12 github.com/open-feature/schemas v0.0.0-20220809125026-ba7ea0aa4841 github.com/robfig/cron v1.2.0 + github.com/rs/cors v1.8.2 github.com/sirupsen/logrus v1.8.1 github.com/soheilhy/cmux v0.1.5 github.com/spf13/cobra v1.4.0 diff --git a/go.sum b/go.sum index bca494cca..73b41ab93 100644 --- a/go.sum +++ b/go.sum @@ -306,6 +306,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= +github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= diff --git a/pkg/runtime/from_config.go b/pkg/runtime/from_config.go index 7f59f7175..8fcc069f0 100644 --- a/pkg/runtime/from_config.go +++ b/pkg/runtime/from_config.go @@ -63,7 +63,14 @@ func (r *Runtime) setServiceFromConfig() error { }), } case "connect": - r.Service = &service.ConnectService{} + r.Service = &service.Service{ + ServiceConfiguration: &service.ServiceConfiguration{ + Port: r.config.ServicePort, + ServerKeyPath: r.config.ServiceKeyPath, + ServerCertPath: r.config.ServiceCertPath, + ServerSocketPath: r.config.ServiceSocketPath, + }, + } default: return errors.New("no service-provider set") } diff --git a/pkg/service/connect_service.go b/pkg/service/connect_service.go index 2320bd524..89231f022 100644 --- a/pkg/service/connect_service.go +++ b/pkg/service/connect_service.go @@ -2,10 +2,15 @@ package service import ( "context" + "errors" + "fmt" + "net" "net/http" + "time" "github.com/bufbuild/connect-go" "github.com/open-feature/flagd/pkg/eval" + "github.com/rs/cors" log "github.com/sirupsen/logrus" schemaV1 "go.buf.build/bufbuild/connect-go/open-feature/flagd/schema/v1" schemaConnectV1 "go.buf.build/bufbuild/connect-go/open-feature/flagd/schema/v1/schemav1connect" @@ -14,23 +19,75 @@ import ( "google.golang.org/protobuf/types/known/structpb" ) -type ConnectService struct { - Eval eval.IEvaluator +type Service struct { + Eval eval.IEvaluator + ServiceConfiguration *ServiceConfiguration } -func (s *ConnectService) Serve(ctx context.Context, eval eval.IEvaluator) error { +type ServiceConfiguration struct { + Port int32 + ServerCertPath string + ServerKeyPath string + ServerSocketPath string +} + +func (s *Service) Serve(ctx context.Context, eval eval.IEvaluator) error { + var address string + var handler http.Handler + tls := false + s.Eval = eval mux := http.NewServeMux() + // sockets + if s.ServiceConfiguration.ServerSocketPath != "" { + address = s.ServiceConfiguration.ServerSocketPath + } else { + address = net.JoinHostPort("localhost", fmt.Sprintf("%d", s.ServiceConfiguration.Port)) + } + // TLS path, handler := schemaConnectV1.NewServiceHandler(s) mux.Handle(path, handler) - return http.ListenAndServe( - "localhost:8080", - // Use h2c so we can serve HTTP/2 without TLS. - h2c.NewHandler(mux, &http2.Server{}), - ) + if s.ServiceConfiguration.ServerCertPath != "" && s.ServiceConfiguration.ServerKeyPath != "" { + tls = true + handler = newCORS().Handler(mux) + } else { + handler = h2c.NewHandler( + newCORS().Handler(mux), + &http2.Server{}, + ) + } + + srv := &http.Server{ + Addr: address, + Handler: handler, + ReadHeaderTimeout: time.Second, + ReadTimeout: 5 * time.Minute, + WriteTimeout: 5 * time.Minute, + MaxHeaderBytes: 8 * 1024, // 8KiB + } + + errChan := make(chan error, 1) + go func() { + if tls { + if err := srv.ListenAndServeTLS(s.ServiceConfiguration.ServerCertPath, s.ServiceConfiguration.ServerKeyPath); err != nil && !errors.Is(err, http.ErrServerClosed) { + errChan <- err + } + } else { + if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + errChan <- err + } + } + close(errChan) + }() + + <-ctx.Done() + if err := srv.Shutdown(ctx); err != nil { + return err + } + return <-errChan } -func (s *ConnectService) ResolveBoolean( +func (s *Service) ResolveBoolean( ctx context.Context, req *connect.Request[schemaV1.ResolveBooleanRequest], ) (*connect.Response[schemaV1.ResolveBooleanResponse], error) { @@ -46,7 +103,7 @@ func (s *ConnectService) ResolveBoolean( return res, nil } -func (s *ConnectService) ResolveString( +func (s *Service) ResolveString( ctx context.Context, req *connect.Request[schemaV1.ResolveStringRequest], ) (*connect.Response[schemaV1.ResolveStringResponse], error) { @@ -62,7 +119,7 @@ func (s *ConnectService) ResolveString( return res, nil } -func (s *ConnectService) ResolveInt( +func (s *Service) ResolveInt( ctx context.Context, req *connect.Request[schemaV1.ResolveIntRequest], ) (*connect.Response[schemaV1.ResolveIntResponse], error) { @@ -78,7 +135,7 @@ func (s *ConnectService) ResolveInt( return res, nil } -func (s *ConnectService) ResolveFloat( +func (s *Service) ResolveFloat( ctx context.Context, req *connect.Request[schemaV1.ResolveFloatRequest], ) (*connect.Response[schemaV1.ResolveFloatResponse], error) { @@ -94,7 +151,7 @@ func (s *ConnectService) ResolveFloat( return res, nil } -func (s *ConnectService) ResolveObject( +func (s *Service) ResolveObject( ctx context.Context, req *connect.Request[schemaV1.ResolveObjectRequest], ) (*connect.Response[schemaV1.ResolveObjectResponse], error) { @@ -113,3 +170,37 @@ func (s *ConnectService) ResolveObject( res.Msg.Variant = variant return res, nil } + +func newCORS() *cors.Cors { + // To let web developers play with the demo service from browsers, we need a + // very permissive CORS setup. + return cors.New(cors.Options{ + AllowedMethods: []string{ + http.MethodHead, + http.MethodGet, + http.MethodPost, + http.MethodPut, + http.MethodPatch, + http.MethodDelete, + }, + AllowOriginFunc: func(origin string) bool { + // Allow all origins, which effectively disables CORS. + return true + }, + AllowedHeaders: []string{"*"}, + ExposedHeaders: []string{ + // Content-Type is in the default safelist. + "Accept", + "Accept-Encoding", + "Accept-Post", + "Connect-Accept-Encoding", + "Connect-Content-Encoding", + "Content-Encoding", + "Grpc-Accept-Encoding", + "Grpc-Encoding", + "Grpc-Message", + "Grpc-Status", + "Grpc-Status-Details-Bin", + }, + }) +} From 45ea73dcd32da3947bb2acec89f0f50e67f7342a Mon Sep 17 00:00:00 2001 From: James-Milligan Date: Tue, 20 Sep 2022 11:28:40 +0100 Subject: [PATCH 03/21] unit tests + fix unix support Signed-off-by: James-Milligan --- cmd/start.go | 20 +- go.mod | 1 + go.sum | 2 + pkg/runtime/from_config.go | 54 +---- pkg/runtime/runtime.go | 1 - pkg/service/connect_service.go | 68 +++--- ...ervice_test.go => connect_service_test.go} | 219 ++++++++---------- pkg/service/grpc_service.go | 171 -------------- pkg/service/http_service.go | 194 ---------------- 9 files changed, 149 insertions(+), 581 deletions(-) rename pkg/service/{grpc_service_test.go => connect_service_test.go} (72%) delete mode 100644 pkg/service/grpc_service.go delete mode 100644 pkg/service/http_service.go diff --git a/cmd/start.go b/cmd/start.go index 049f77544..e09b68193 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -11,15 +11,14 @@ import ( ) const ( - portFlagName = "port" - serviceProviderFlagName = "service-provider" - socketPathFlagName = "socket-path" - syncProviderFlagName = "sync-provider" - evaluatorFlagName = "evaluator" - serverCertPathFlagName = "server-cert-path" - serverKeyPathFlagName = "server-key-path" - uriFlagName = "uri" - bearerTokenFlagName = "bearer-token" + portFlagName = "port" + socketPathFlagName = "socket-path" + syncProviderFlagName = "sync-provider" + evaluatorFlagName = "evaluator" + serverCertPathFlagName = "server-cert-path" + serverKeyPathFlagName = "server-key-path" + uriFlagName = "uri" + bearerTokenFlagName = "bearer-token" ) func init() { @@ -33,7 +32,6 @@ func init() { flags.StringP(socketPathFlagName, "d", "", "Flagd socket path. "+ "With grpc the service will become available on this address. "+ "With http(s) the grpc-gateway proxy will use this address internally.") - flags.StringP(serviceProviderFlagName, "s", "http", "Set a service provider e.g. http or grpc") flags.StringP( syncProviderFlagName, "y", "filepath", "Set a sync provider e.g. filepath or remote", ) @@ -49,7 +47,6 @@ func init() { _ = viper.BindPFlag(portFlagName, flags.Lookup(portFlagName)) _ = viper.BindPFlag(socketPathFlagName, flags.Lookup(socketPathFlagName)) - _ = viper.BindPFlag(serviceProviderFlagName, flags.Lookup(serviceProviderFlagName)) _ = viper.BindPFlag(syncProviderFlagName, flags.Lookup(syncProviderFlagName)) _ = viper.BindPFlag(evaluatorFlagName, flags.Lookup(evaluatorFlagName)) _ = viper.BindPFlag(serverCertPathFlagName, flags.Lookup(serverCertPathFlagName)) @@ -73,7 +70,6 @@ var startCmd = &cobra.Command{ // Build Runtime ----------------------------------------------------------- rt, err := runtime.FromConfig(runtime.Config{ - ServiceProvider: viper.GetString(serviceProviderFlagName), ServicePort: viper.GetInt32(portFlagName), ServiceSocketPath: viper.GetString(socketPathFlagName), ServiceCertPath: viper.GetString(serverCertPathFlagName), diff --git a/go.mod b/go.mod index 5e725534e..102098641 100644 --- a/go.mod +++ b/go.mod @@ -55,6 +55,7 @@ require ( github.com/subosito/gotenv v1.3.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + go.buf.build/open-feature/flagd-connect/open-feature/flagd v1.1.3 // indirect golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c // indirect golang.org/x/text v0.3.7 // indirect google.golang.org/genproto v0.0.0-20220728213248-dd149ef739b9 // indirect diff --git a/go.sum b/go.sum index 73b41ab93..2c10585c2 100644 --- a/go.sum +++ b/go.sum @@ -362,6 +362,8 @@ github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.buf.build/bufbuild/connect-go/open-feature/flagd v1.7.2 h1:IzQZ2lGT44mLOB8roKxQHEOZShMRZNH49cRBZGavaxw= go.buf.build/bufbuild/connect-go/open-feature/flagd v1.7.2/go.mod h1:VpawqcW+HY3faAQvWpTJQOdTGT+w8pCWVif9C4Jd35M= +go.buf.build/open-feature/flagd-connect/open-feature/flagd v1.1.3 h1:WdB8KPnlU7S+ux7euBKg6k4ZSrEiEHBJBJLnD5otxQM= +go.buf.build/open-feature/flagd-connect/open-feature/flagd v1.1.3/go.mod h1:ZaEOUjTu/oxTFHuwNjF2788REepgWYPsU4Ty6lPIMzc= go.buf.build/open-feature/flagd-server/open-feature/flagd v1.1.2 h1:GbrVEHuAp/3a6JpfoV8cusP4/CveInar997DCCbXSAg= go.buf.build/open-feature/flagd-server/open-feature/flagd v1.1.2/go.mod h1:1gOZbwcemK1/404A8u4kXmzIn4DIIT1xIe8JuowznT8= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= diff --git a/pkg/runtime/from_config.go b/pkg/runtime/from_config.go index 8fcc069f0..cd64973f5 100644 --- a/pkg/runtime/from_config.go +++ b/pkg/runtime/from_config.go @@ -24,58 +24,22 @@ func FromConfig(config Config) (*Runtime, error) { if err := rt.setEvaluatorFromConfig(); err != nil { return nil, err } - if err := rt.setServiceFromConfig(); err != nil { - return nil, err - } if err := rt.setSyncImplFromConfig(); err != nil { return nil, err } + rt.setService() return &rt, nil } -func (r *Runtime) setServiceFromConfig() error { - switch r.config.ServiceProvider { - case "http": - r.Service = &service.HTTPService{ - HTTPServiceConfiguration: &service.HTTPServiceConfiguration{ - Port: r.config.ServicePort, - ServerKeyPath: r.config.ServiceKeyPath, - ServerCertPath: r.config.ServiceCertPath, - ServerSocketPath: r.config.ServiceSocketPath, - }, - GRPCService: &service.GRPCService{}, - Logger: log.WithFields(log.Fields{ - "service": "http", - "component": "service", - }), - } - case "grpc": - r.Service = &service.GRPCService{ - GRPCServiceConfiguration: &service.GRPCServiceConfiguration{ - Port: r.config.ServicePort, - ServerKeyPath: r.config.ServiceKeyPath, - ServerCertPath: r.config.ServiceCertPath, - ServerSocketPath: r.config.ServiceSocketPath, - }, - Logger: log.WithFields(log.Fields{ - "service": "grpc", - "component": "service", - }), - } - case "connect": - r.Service = &service.Service{ - ServiceConfiguration: &service.ServiceConfiguration{ - Port: r.config.ServicePort, - ServerKeyPath: r.config.ServiceKeyPath, - ServerCertPath: r.config.ServiceCertPath, - ServerSocketPath: r.config.ServiceSocketPath, - }, - } - default: - return errors.New("no service-provider set") +func (r *Runtime) setService() { + r.Service = &service.ConnectService{ + ConnectServiceConfiguration: &service.ConnectServiceConfiguration{ + Port: r.config.ServicePort, + ServerKeyPath: r.config.ServiceKeyPath, + ServerCertPath: r.config.ServiceCertPath, + ServerSocketPath: r.config.ServiceSocketPath, + }, } - log.Debugf("Using %s service-provider\n", r.config.ServiceProvider) - return nil } func (r *Runtime) setEvaluatorFromConfig() error { diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go index 9dda6df49..5f30f5c14 100644 --- a/pkg/runtime/runtime.go +++ b/pkg/runtime/runtime.go @@ -22,7 +22,6 @@ type Runtime struct { } type Config struct { - ServiceProvider string ServicePort int32 ServiceSocketPath string ServiceCertPath string diff --git a/pkg/service/connect_service.go b/pkg/service/connect_service.go index 89231f022..f2a4aab06 100644 --- a/pkg/service/connect_service.go +++ b/pkg/service/connect_service.go @@ -6,48 +6,55 @@ import ( "fmt" "net" "net/http" - "time" "github.com/bufbuild/connect-go" "github.com/open-feature/flagd/pkg/eval" "github.com/rs/cors" log "github.com/sirupsen/logrus" - schemaV1 "go.buf.build/bufbuild/connect-go/open-feature/flagd/schema/v1" - schemaConnectV1 "go.buf.build/bufbuild/connect-go/open-feature/flagd/schema/v1/schemav1connect" + schemaV1 "go.buf.build/open-feature/flagd-connect/open-feature/flagd/schema/v1" + schemaConnectV1 "go.buf.build/open-feature/flagd-connect/open-feature/flagd/schema/v1/schemav1connect" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" "google.golang.org/protobuf/types/known/structpb" ) -type Service struct { - Eval eval.IEvaluator - ServiceConfiguration *ServiceConfiguration +type ConnectService struct { + Eval eval.IEvaluator + ConnectServiceConfiguration *ConnectServiceConfiguration } -type ServiceConfiguration struct { +type ConnectServiceConfiguration struct { Port int32 ServerCertPath string ServerKeyPath string ServerSocketPath string } -func (s *Service) Serve(ctx context.Context, eval eval.IEvaluator) error { - var address string +func (s *ConnectService) Serve(ctx context.Context, eval eval.IEvaluator) error { var handler http.Handler + var lis net.Listener + var err error + tls := false s.Eval = eval mux := http.NewServeMux() // sockets - if s.ServiceConfiguration.ServerSocketPath != "" { - address = s.ServiceConfiguration.ServerSocketPath + + if s.ConnectServiceConfiguration.ServerSocketPath != "" { + lis, err = net.Listen("unix", s.ConnectServiceConfiguration.ServerSocketPath) } else { - address = net.JoinHostPort("localhost", fmt.Sprintf("%d", s.ServiceConfiguration.Port)) + address := net.JoinHostPort("localhost", fmt.Sprintf("%d", s.ConnectServiceConfiguration.Port)) + lis, err = net.Listen("tcp", address) + } + if err != nil { + return err } // TLS path, handler := schemaConnectV1.NewServiceHandler(s) mux.Handle(path, handler) - if s.ServiceConfiguration.ServerCertPath != "" && s.ServiceConfiguration.ServerKeyPath != "" { + + if s.ConnectServiceConfiguration.ServerCertPath != "" && s.ConnectServiceConfiguration.ServerKeyPath != "" { tls = true handler = newCORS().Handler(mux) } else { @@ -57,23 +64,22 @@ func (s *Service) Serve(ctx context.Context, eval eval.IEvaluator) error { ) } - srv := &http.Server{ - Addr: address, - Handler: handler, - ReadHeaderTimeout: time.Second, - ReadTimeout: 5 * time.Minute, - WriteTimeout: 5 * time.Minute, - MaxHeaderBytes: 8 * 1024, // 8KiB - } - errChan := make(chan error, 1) go func() { if tls { - if err := srv.ListenAndServeTLS(s.ServiceConfiguration.ServerCertPath, s.ServiceConfiguration.ServerKeyPath); err != nil && !errors.Is(err, http.ErrServerClosed) { + if err := http.ServeTLS( + lis, + handler, + s.ConnectServiceConfiguration.ServerCertPath, + s.ConnectServiceConfiguration.ServerKeyPath, + ); err != nil && !errors.Is(err, http.ErrServerClosed) { errChan <- err } } else { - if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + if err := http.Serve( + lis, + handler, + ); err != nil && !errors.Is(err, http.ErrServerClosed) { errChan <- err } } @@ -81,13 +87,13 @@ func (s *Service) Serve(ctx context.Context, eval eval.IEvaluator) error { }() <-ctx.Done() - if err := srv.Shutdown(ctx); err != nil { + if err := lis.Close(); err != nil { return err } return <-errChan } -func (s *Service) ResolveBoolean( +func (s *ConnectService) ResolveBoolean( ctx context.Context, req *connect.Request[schemaV1.ResolveBooleanRequest], ) (*connect.Response[schemaV1.ResolveBooleanResponse], error) { @@ -103,7 +109,7 @@ func (s *Service) ResolveBoolean( return res, nil } -func (s *Service) ResolveString( +func (s *ConnectService) ResolveString( ctx context.Context, req *connect.Request[schemaV1.ResolveStringRequest], ) (*connect.Response[schemaV1.ResolveStringResponse], error) { @@ -119,7 +125,7 @@ func (s *Service) ResolveString( return res, nil } -func (s *Service) ResolveInt( +func (s *ConnectService) ResolveInt( ctx context.Context, req *connect.Request[schemaV1.ResolveIntRequest], ) (*connect.Response[schemaV1.ResolveIntResponse], error) { @@ -135,7 +141,7 @@ func (s *Service) ResolveInt( return res, nil } -func (s *Service) ResolveFloat( +func (s *ConnectService) ResolveFloat( ctx context.Context, req *connect.Request[schemaV1.ResolveFloatRequest], ) (*connect.Response[schemaV1.ResolveFloatResponse], error) { @@ -151,7 +157,7 @@ func (s *Service) ResolveFloat( return res, nil } -func (s *Service) ResolveObject( +func (s *ConnectService) ResolveObject( ctx context.Context, req *connect.Request[schemaV1.ResolveObjectRequest], ) (*connect.Response[schemaV1.ResolveObjectResponse], error) { @@ -172,7 +178,7 @@ func (s *Service) ResolveObject( } func newCORS() *cors.Cors { - // To let web developers play with the demo service from browsers, we need a + // To let web developers play with the demo ConnectService from browsers, we need a // very permissive CORS setup. return cors.New(cors.Options{ AllowedMethods: []string{ diff --git a/pkg/service/grpc_service_test.go b/pkg/service/connect_service_test.go similarity index 72% rename from pkg/service/grpc_service_test.go rename to pkg/service/connect_service_test.go index 6c102b067..ec5742d99 100644 --- a/pkg/service/grpc_service_test.go +++ b/pkg/service/connect_service_test.go @@ -8,10 +8,11 @@ import ( "testing" "time" + "github.com/bufbuild/connect-go" "github.com/golang/mock/gomock" service "github.com/open-feature/flagd/pkg/service" log "github.com/sirupsen/logrus" - gen "go.buf.build/open-feature/flagd-server/open-feature/flagd/schema/v1" + gen "go.buf.build/open-feature/flagd-connect/open-feature/flagd/schema/v1" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/protobuf/types/known/structpb" @@ -31,10 +32,9 @@ type resolveBooleanEvalFields struct { result bool variant string reason string - err error } -func TestGRPCService_UnixConnection(t *testing.T) { +func TestConnectService_UnixConnection(t *testing.T) { type evalFields struct { result bool variant string @@ -60,7 +60,7 @@ func TestGRPCService_UnixConnection(t *testing.T) { err: nil, }, req: &gen.ResolveBooleanRequest{ - FlagKey: "bool", + FlagKey: "myBoolFlag", Context: &structpb.Struct{}, }, want: &gen.ResolveBooleanResponse{ @@ -81,8 +81,8 @@ func TestGRPCService_UnixConnection(t *testing.T) { tt.evalFields.reason, tt.evalFields.err, ).AnyTimes() - service := service.GRPCService{ - GRPCServiceConfiguration: &service.GRPCServiceConfiguration{ + service := service.ConnectService{ + ConnectServiceConfiguration: &service.ConnectServiceConfiguration{ ServerSocketPath: tt.socketPath, }, } @@ -91,9 +91,8 @@ func TestGRPCService_UnixConnection(t *testing.T) { defer cancel() go func() { _ = service.Serve(ctx, eval) }() - conn, err := grpc.Dial( - fmt.Sprintf("passthrough:///unix://%s", tt.socketPath), + fmt.Sprintf("unix://%s", tt.socketPath), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock(), ) @@ -106,32 +105,30 @@ func TestGRPCService_UnixConnection(t *testing.T) { ) res, err := client.ResolveBoolean(ctx, tt.req) if (err != nil) && !errors.Is(err, tt.wantErr) { - t.Errorf("GRPCService.ResolveBoolean() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("ConnectService.ResolveBoolean() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(res.Reason, tt.want.Reason) { - t.Errorf("GRPCService.ResolveBoolean() = %v, want %v", res, tt.want) + t.Errorf("ConnectService.ResolveBoolean() = %v, want %v", res, tt.want) } if !reflect.DeepEqual(res.Value, tt.want.Value) { - t.Errorf("GRPCService.ResolveBoolean() = %v, want %v", res, tt.want) + t.Errorf("ConnectService.ResolveBoolean() = %v, want %v", res, tt.want) } if !reflect.DeepEqual(res.Variant, tt.want.Variant) { - t.Errorf("GRPCService.ResolveBoolean() = %v, want %v", res, tt.want) + t.Errorf("ConnectService.ResolveBoolean() = %v, want %v", res, tt.want) } }) } } -func TestGRPCService_ResolveBoolean(t *testing.T) { +func TestConnectService_ResolveBoolean(t *testing.T) { ctrl := gomock.NewController(t) - grpcS := service.GRPCService{} tests := map[string]resolveBooleanArgs{ "happy path": { evalFields: resolveBooleanEvalFields{ result: true, variant: "on", reason: "STATIC", - err: nil, }, functionArgs: resolveBooleanFunctionArgs{ context.Background(), @@ -152,7 +149,6 @@ func TestGRPCService_ResolveBoolean(t *testing.T) { result: true, variant: ":(", reason: "ERROR", - err: errors.New("eval interface error"), }, functionArgs: resolveBooleanFunctionArgs{ context.Background(), @@ -162,7 +158,7 @@ func TestGRPCService_ResolveBoolean(t *testing.T) { }, }, want: &gen.ResolveBooleanResponse{}, - wantErr: grpcS.HandleEvaluationError(errors.New("eval interface error"), "ERROR"), + wantErr: errors.New("eval interface error"), }, } for name, tt := range tests { @@ -172,33 +168,31 @@ func TestGRPCService_ResolveBoolean(t *testing.T) { tt.evalFields.result, tt.evalFields.variant, tt.evalFields.reason, - tt.evalFields.err, + tt.wantErr, ).AnyTimes() - s := service.GRPCService{ + s := service.ConnectService{ Eval: eval, } - got, err := s.ResolveBoolean(tt.functionArgs.ctx, tt.functionArgs.req) - if (err != nil) && !errors.Is(err, tt.wantErr) { - t.Errorf("GRPCService.ResolveBoolean() error = %v, wantErr %v", err, tt.wantErr) + got, err := s.ResolveBoolean(tt.functionArgs.ctx, connect.NewRequest(tt.functionArgs.req)) + if err != nil && !errors.Is(err, tt.wantErr) { + t.Errorf("ConnectService.ResolveBoolean() error = %v, wantErr %v", err.Error(), tt.wantErr.Error()) return } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("GRPCService.ResolveBoolean() = %v, want %v", got, tt.want) + if !reflect.DeepEqual(got.Msg, tt.want) { + t.Errorf("ConnectService.ResolveBoolean() = %v, want %v", got, tt.want) } }) } } -func BenchmarkGRPCService_ResolveBoolean(b *testing.B) { +func BenchmarkConnectService_ResolveBoolean(b *testing.B) { ctrl := gomock.NewController(b) - grpcS := service.GRPCService{} tests := map[string]resolveBooleanArgs{ "happy path": { evalFields: resolveBooleanEvalFields{ result: true, variant: "on", reason: "STATIC", - err: nil, }, functionArgs: resolveBooleanFunctionArgs{ context.Background(), @@ -219,7 +213,6 @@ func BenchmarkGRPCService_ResolveBoolean(b *testing.B) { result: true, variant: ":(", reason: "ERROR", - err: errors.New("eval interface error"), }, functionArgs: resolveBooleanFunctionArgs{ context.Background(), @@ -229,7 +222,7 @@ func BenchmarkGRPCService_ResolveBoolean(b *testing.B) { }, }, want: &gen.ResolveBooleanResponse{}, - wantErr: grpcS.HandleEvaluationError(errors.New("eval interface error"), "ERROR"), + wantErr: errors.New("eval interface error"), }, } for name, tt := range tests { @@ -238,20 +231,20 @@ func BenchmarkGRPCService_ResolveBoolean(b *testing.B) { tt.evalFields.result, tt.evalFields.variant, tt.evalFields.reason, - tt.evalFields.err, + tt.wantErr, ).AnyTimes() - s := service.GRPCService{ + s := service.ConnectService{ Eval: eval, } b.Run(name, func(b *testing.B) { for i := 0; i < b.N; i++ { - got, err := s.ResolveBoolean(tt.functionArgs.ctx, tt.functionArgs.req) + got, err := s.ResolveBoolean(tt.functionArgs.ctx, connect.NewRequest(tt.functionArgs.req)) if (err != nil) && !errors.Is(err, tt.wantErr) { - b.Errorf("GRPCService.ResolveBoolean() error = %v, wantErr %v", err, tt.wantErr) + b.Errorf("ConnectService.ResolveBoolean() error = %v, wantErr %v", err, tt.wantErr) return } - if !reflect.DeepEqual(got, tt.want) { - b.Errorf("GRPCService.ResolveBoolean() = %v, want %v", got, tt.want) + if !reflect.DeepEqual(got.Msg, tt.want) { + b.Errorf("ConnectService.ResolveBoolean() = %v, want %v", got, tt.want) } } }) @@ -272,19 +265,16 @@ type resolveStringEvalFields struct { result string variant string reason string - err error } -func TestGRPCService_ResolveString(t *testing.T) { +func TestConnectService_ResolveString(t *testing.T) { ctrl := gomock.NewController(t) - grpcS := service.GRPCService{} tests := map[string]resolveStringArgs{ "happy path": { evalFields: resolveStringEvalFields{ result: "true", variant: "on", reason: "STATIC", - err: nil, }, functionArgs: resolveStringFunctionArgs{ context.Background(), @@ -305,7 +295,6 @@ func TestGRPCService_ResolveString(t *testing.T) { result: "true", variant: ":(", reason: "ERROR", - err: errors.New("eval interface error"), }, functionArgs: resolveStringFunctionArgs{ context.Background(), @@ -315,7 +304,7 @@ func TestGRPCService_ResolveString(t *testing.T) { }, }, want: &gen.ResolveStringResponse{}, - wantErr: grpcS.HandleEvaluationError(errors.New("eval interface error"), "ERROR"), + wantErr: errors.New("eval interface error"), }, } for name, tt := range tests { @@ -325,33 +314,31 @@ func TestGRPCService_ResolveString(t *testing.T) { tt.evalFields.result, tt.evalFields.variant, tt.evalFields.reason, - tt.evalFields.err, + tt.wantErr, ) - s := service.GRPCService{ + s := service.ConnectService{ Eval: eval, } - got, err := s.ResolveString(tt.functionArgs.ctx, tt.functionArgs.req) + got, err := s.ResolveString(tt.functionArgs.ctx, connect.NewRequest(tt.functionArgs.req)) if (err != nil) && !errors.Is(err, tt.wantErr) { - t.Errorf("GRPCService.ResolveString() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("ConnectService.ResolveString() error = %v, wantErr %v", err, tt.wantErr) return } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("GRPCService.ResolveString() = %v, want %v", got, tt.want) + if !reflect.DeepEqual(got.Msg, tt.want) { + t.Errorf("ConnectService.ResolveString() = %v, want %v", got, tt.want) } }) } } -func BenchmarkGRPCService_ResolveString(b *testing.B) { +func BenchmarkConnectService_ResolveString(b *testing.B) { ctrl := gomock.NewController(b) - grpcS := service.GRPCService{} tests := map[string]resolveStringArgs{ "happy path": { evalFields: resolveStringEvalFields{ result: "true", variant: "on", reason: "STATIC", - err: nil, }, functionArgs: resolveStringFunctionArgs{ context.Background(), @@ -372,7 +359,6 @@ func BenchmarkGRPCService_ResolveString(b *testing.B) { result: "true", variant: ":(", reason: "ERROR", - err: errors.New("eval interface error"), }, functionArgs: resolveStringFunctionArgs{ context.Background(), @@ -382,7 +368,7 @@ func BenchmarkGRPCService_ResolveString(b *testing.B) { }, }, want: &gen.ResolveStringResponse{}, - wantErr: grpcS.HandleEvaluationError(errors.New("eval interface error"), "ERROR"), + wantErr: errors.New("eval interface error"), }, } for name, tt := range tests { @@ -391,20 +377,20 @@ func BenchmarkGRPCService_ResolveString(b *testing.B) { tt.evalFields.result, tt.evalFields.variant, tt.evalFields.reason, - tt.evalFields.err, + tt.wantErr, ).AnyTimes() - s := service.GRPCService{ + s := service.ConnectService{ Eval: eval, } b.Run(name, func(b *testing.B) { for i := 0; i < b.N; i++ { - got, err := s.ResolveString(tt.functionArgs.ctx, tt.functionArgs.req) + got, err := s.ResolveString(tt.functionArgs.ctx, connect.NewRequest(tt.functionArgs.req)) if (err != nil) && !errors.Is(err, tt.wantErr) { - b.Errorf("GRPCService.ResolveString() error = %v, wantErr %v", err, tt.wantErr) + b.Errorf("ConnectService.ResolveString() error = %v, wantErr %v", err, tt.wantErr) return } - if !reflect.DeepEqual(got, tt.want) { - b.Errorf("GRPCService.ResolveString() = %v, want %v", got, tt.want) + if !reflect.DeepEqual(got.Msg, tt.want) { + b.Errorf("ConnectService.ResolveString() = %v, want %v", got, tt.want) } } }) @@ -425,19 +411,16 @@ type resolveFloatEvalFields struct { result float64 variant string reason string - err error } -func TestGRPCService_ResolveFloat(t *testing.T) { +func TestConnectService_ResolveFloat(t *testing.T) { ctrl := gomock.NewController(t) - grpcS := service.GRPCService{} tests := map[string]resolveFloatArgs{ "happy path": { evalFields: resolveFloatEvalFields{ result: 12, variant: "on", reason: "STATIC", - err: nil, }, functionArgs: resolveFloatFunctionArgs{ context.Background(), @@ -458,7 +441,6 @@ func TestGRPCService_ResolveFloat(t *testing.T) { result: 12, variant: ":(", reason: "ERROR", - err: errors.New("eval interface error"), }, functionArgs: resolveFloatFunctionArgs{ context.Background(), @@ -468,7 +450,7 @@ func TestGRPCService_ResolveFloat(t *testing.T) { }, }, want: &gen.ResolveFloatResponse{}, - wantErr: grpcS.HandleEvaluationError(errors.New("eval interface error"), "ERROR"), + wantErr: errors.New("eval interface error"), }, } for name, tt := range tests { @@ -478,33 +460,31 @@ func TestGRPCService_ResolveFloat(t *testing.T) { tt.evalFields.result, tt.evalFields.variant, tt.evalFields.reason, - tt.evalFields.err, + tt.wantErr, ).AnyTimes() - s := service.GRPCService{ + s := service.ConnectService{ Eval: eval, } - got, err := s.ResolveFloat(tt.functionArgs.ctx, tt.functionArgs.req) + got, err := s.ResolveFloat(tt.functionArgs.ctx, connect.NewRequest(tt.functionArgs.req)) if (err != nil) && !errors.Is(err, tt.wantErr) { - t.Errorf("GRPCService.ResolveNumber() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("ConnectService.ResolveNumber() error = %v, wantErr %v", err, tt.wantErr) return } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("GRPCService.ResolveNumber() = %v, want %v", got, tt.want) + if !reflect.DeepEqual(got.Msg, tt.want) { + t.Errorf("ConnectService.ResolveNumber() = %v, want %v", got, tt.want) } }) } } -func BenchmarkGRPCService_ResolveFloat(b *testing.B) { +func BenchmarkConnectService_ResolveFloat(b *testing.B) { ctrl := gomock.NewController(b) - grpcS := service.GRPCService{} tests := map[string]resolveFloatArgs{ "happy path": { evalFields: resolveFloatEvalFields{ result: 12, variant: "on", reason: "STATIC", - err: nil, }, functionArgs: resolveFloatFunctionArgs{ context.Background(), @@ -525,7 +505,6 @@ func BenchmarkGRPCService_ResolveFloat(b *testing.B) { result: 12, variant: ":(", reason: "ERROR", - err: errors.New("eval interface error"), }, functionArgs: resolveFloatFunctionArgs{ context.Background(), @@ -535,7 +514,7 @@ func BenchmarkGRPCService_ResolveFloat(b *testing.B) { }, }, want: &gen.ResolveFloatResponse{}, - wantErr: grpcS.HandleEvaluationError(errors.New("eval interface error"), "ERROR"), + wantErr: errors.New("eval interface error"), }, } for name, tt := range tests { @@ -544,20 +523,20 @@ func BenchmarkGRPCService_ResolveFloat(b *testing.B) { tt.evalFields.result, tt.evalFields.variant, tt.evalFields.reason, - tt.evalFields.err, + tt.wantErr, ).AnyTimes() - s := service.GRPCService{ + s := service.ConnectService{ Eval: eval, } b.Run(name, func(b *testing.B) { for i := 0; i < b.N; i++ { - got, err := s.ResolveFloat(tt.functionArgs.ctx, tt.functionArgs.req) + got, err := s.ResolveFloat(tt.functionArgs.ctx, connect.NewRequest(tt.functionArgs.req)) if (err != nil) && !errors.Is(err, tt.wantErr) { - b.Errorf("GRPCService.ResolveNumber() error = %v, wantErr %v", err, tt.wantErr) + b.Errorf("ConnectService.ResolveNumber() error = %v, wantErr %v", err, tt.wantErr) return } - if !reflect.DeepEqual(got, tt.want) { - b.Errorf("GRPCService.ResolveNumber() = %v, want %v", got, tt.want) + if !reflect.DeepEqual(got.Msg, tt.want) { + b.Errorf("ConnectService.ResolveNumber() = %v, want %v", got, tt.want) } } }) @@ -578,19 +557,16 @@ type resolveIntEvalFields struct { result int64 variant string reason string - err error } -func TestGRPCService_ResolveInt(t *testing.T) { +func TestConnectService_ResolveInt(t *testing.T) { ctrl := gomock.NewController(t) - grpcS := service.GRPCService{} tests := map[string]resolveIntArgs{ "happy path": { evalFields: resolveIntEvalFields{ result: 12, variant: "on", reason: "STATIC", - err: nil, }, functionArgs: resolveIntFunctionArgs{ context.Background(), @@ -611,7 +587,6 @@ func TestGRPCService_ResolveInt(t *testing.T) { result: 12, variant: ":(", reason: "ERROR", - err: errors.New("eval interface error"), }, functionArgs: resolveIntFunctionArgs{ context.Background(), @@ -621,7 +596,7 @@ func TestGRPCService_ResolveInt(t *testing.T) { }, }, want: &gen.ResolveIntResponse{}, - wantErr: grpcS.HandleEvaluationError(errors.New("eval interface error"), "ERROR"), + wantErr: errors.New("eval interface error"), }, } for name, tt := range tests { @@ -631,33 +606,31 @@ func TestGRPCService_ResolveInt(t *testing.T) { tt.evalFields.result, tt.evalFields.variant, tt.evalFields.reason, - tt.evalFields.err, + tt.wantErr, ).AnyTimes() - s := service.GRPCService{ + s := service.ConnectService{ Eval: eval, } - got, err := s.ResolveInt(tt.functionArgs.ctx, tt.functionArgs.req) + got, err := s.ResolveInt(tt.functionArgs.ctx, connect.NewRequest(tt.functionArgs.req)) if (err != nil) && !errors.Is(err, tt.wantErr) { - t.Errorf("GRPCService.ResolveNumber() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("ConnectService.ResolveNumber() error = %v, wantErr %v", err, tt.wantErr) return } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("GRPCService.ResolveNumber() = %v, want %v", got, tt.want) + if !reflect.DeepEqual(got.Msg, tt.want) { + t.Errorf("ConnectService.ResolveNumber() = %v, want %v", got, tt.want) } }) } } -func BenchmarkGRPCService_ResolveInt(b *testing.B) { +func BenchmarkConnectService_ResolveInt(b *testing.B) { ctrl := gomock.NewController(b) - grpcS := service.GRPCService{} tests := map[string]resolveIntArgs{ "happy path": { evalFields: resolveIntEvalFields{ result: 12, variant: "on", reason: "STATIC", - err: nil, }, functionArgs: resolveIntFunctionArgs{ context.Background(), @@ -678,7 +651,6 @@ func BenchmarkGRPCService_ResolveInt(b *testing.B) { result: 12, variant: ":(", reason: "ERROR", - err: errors.New("eval interface error"), }, functionArgs: resolveIntFunctionArgs{ context.Background(), @@ -688,7 +660,7 @@ func BenchmarkGRPCService_ResolveInt(b *testing.B) { }, }, want: &gen.ResolveIntResponse{}, - wantErr: grpcS.HandleEvaluationError(errors.New("eval interface error"), "ERROR"), + wantErr: errors.New("eval interface error"), }, } for name, tt := range tests { @@ -697,20 +669,20 @@ func BenchmarkGRPCService_ResolveInt(b *testing.B) { tt.evalFields.result, tt.evalFields.variant, tt.evalFields.reason, - tt.evalFields.err, + tt.wantErr, ).AnyTimes() - s := service.GRPCService{ + s := service.ConnectService{ Eval: eval, } b.Run(name, func(b *testing.B) { for i := 0; i < b.N; i++ { - got, err := s.ResolveInt(tt.functionArgs.ctx, tt.functionArgs.req) + got, err := s.ResolveInt(tt.functionArgs.ctx, connect.NewRequest(tt.functionArgs.req)) if (err != nil) && !errors.Is(err, tt.wantErr) { - b.Errorf("GRPCService.ResolveNumber() error = %v, wantErr %v", err, tt.wantErr) + b.Errorf("ConnectService.ResolveNumber() error = %v, wantErr %v", err, tt.wantErr) return } - if !reflect.DeepEqual(got, tt.want) { - b.Errorf("GRPCService.ResolveNumber() = %v, want %v", got, tt.want) + if !reflect.DeepEqual(got.Msg, tt.want) { + b.Errorf("ConnectService.ResolveNumber() = %v, want %v", got, tt.want) } } }) @@ -731,12 +703,10 @@ type resolveObjectEvalFields struct { result map[string]interface{} variant string reason string - err error } -func TestGRPCService_ResolveObject(t *testing.T) { +func TestConnectService_ResolveObject(t *testing.T) { ctrl := gomock.NewController(t) - grpcS := service.GRPCService{} tests := map[string]resolveObjectArgs{ "happy path": { evalFields: resolveObjectEvalFields{ @@ -745,7 +715,6 @@ func TestGRPCService_ResolveObject(t *testing.T) { }, variant: "on", reason: "STATIC", - err: nil, }, functionArgs: resolveObjectFunctionArgs{ context.Background(), @@ -768,7 +737,6 @@ func TestGRPCService_ResolveObject(t *testing.T) { }, variant: ":(", reason: "ERROR", - err: errors.New("eval interface error"), }, functionArgs: resolveObjectFunctionArgs{ context.Background(), @@ -778,7 +746,7 @@ func TestGRPCService_ResolveObject(t *testing.T) { }, }, want: &gen.ResolveObjectResponse{}, - wantErr: grpcS.HandleEvaluationError(errors.New("eval interface error"), "ERROR"), + wantErr: errors.New("eval interface error"), }, } for name, tt := range tests { @@ -788,9 +756,9 @@ func TestGRPCService_ResolveObject(t *testing.T) { tt.evalFields.result, tt.evalFields.variant, tt.evalFields.reason, - tt.evalFields.err, + tt.wantErr, ).AnyTimes() - s := service.GRPCService{ + s := service.ConnectService{ Eval: eval, } @@ -801,21 +769,20 @@ func TestGRPCService_ResolveObject(t *testing.T) { } tt.want.Value = outParsed } - got, err := s.ResolveObject(tt.functionArgs.ctx, tt.functionArgs.req) + got, err := s.ResolveObject(tt.functionArgs.ctx, connect.NewRequest(tt.functionArgs.req)) if (err != nil) && !errors.Is(err, tt.wantErr) { - t.Errorf("GRPCService.ResolveObject() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("ConnectService.ResolveObject() error = %v, wantErr %v", err, tt.wantErr) return } - if !reflect.DeepEqual(got.Value.AsMap(), tt.want.Value.AsMap()) { - t.Errorf("GRPCService.ResolveObject() = %v, want %v", got, tt.want) + if !reflect.DeepEqual(got.Msg.Value.AsMap(), tt.want.Value.AsMap()) { + t.Errorf("ConnectService.ResolveObject() = %v, want %v", got, tt.want) } }) } } -func BenchmarkGRPCService_ResolveObject(b *testing.B) { +func BenchmarkConnectService_ResolveObject(b *testing.B) { ctrl := gomock.NewController(b) - grpcS := service.GRPCService{} tests := map[string]resolveObjectArgs{ "happy path": { evalFields: resolveObjectEvalFields{ @@ -824,7 +791,6 @@ func BenchmarkGRPCService_ResolveObject(b *testing.B) { }, variant: "on", reason: "STATIC", - err: nil, }, functionArgs: resolveObjectFunctionArgs{ context.Background(), @@ -847,7 +813,6 @@ func BenchmarkGRPCService_ResolveObject(b *testing.B) { }, variant: ":(", reason: "ERROR", - err: errors.New("eval interface error"), }, functionArgs: resolveObjectFunctionArgs{ context.Background(), @@ -857,7 +822,7 @@ func BenchmarkGRPCService_ResolveObject(b *testing.B) { }, }, want: &gen.ResolveObjectResponse{}, - wantErr: grpcS.HandleEvaluationError(errors.New("eval interface error"), "ERROR"), + wantErr: errors.New("eval interface error"), }, } for name, tt := range tests { @@ -866,9 +831,9 @@ func BenchmarkGRPCService_ResolveObject(b *testing.B) { tt.evalFields.result, tt.evalFields.variant, tt.evalFields.reason, - tt.evalFields.err, + tt.wantErr, ).AnyTimes() - s := service.GRPCService{ + s := service.ConnectService{ Eval: eval, } if name != "eval returns error" { @@ -880,13 +845,13 @@ func BenchmarkGRPCService_ResolveObject(b *testing.B) { } b.Run(name, func(b *testing.B) { for i := 0; i < b.N; i++ { - got, err := s.ResolveObject(tt.functionArgs.ctx, tt.functionArgs.req) + got, err := s.ResolveObject(tt.functionArgs.ctx, connect.NewRequest(tt.functionArgs.req)) if (err != nil) && !errors.Is(err, tt.wantErr) { - b.Errorf("GRPCService.ResolveObject() error = %v, wantErr %v", err, tt.wantErr) + b.Errorf("ConnectService.ResolveObject() error = %v, wantErr %v", err, tt.wantErr) return } - if !reflect.DeepEqual(got.Value.AsMap(), tt.want.Value.AsMap()) { - b.Errorf("GRPCService.ResolveObject() = %v, want %v", got, tt.want) + if !reflect.DeepEqual(got.Msg.Value.AsMap(), tt.want.Value.AsMap()) { + b.Errorf("ConnectService.ResolveObject() = %v, want %v", got, tt.want) } } }) diff --git a/pkg/service/grpc_service.go b/pkg/service/grpc_service.go deleted file mode 100644 index 3adaa3190..000000000 --- a/pkg/service/grpc_service.go +++ /dev/null @@ -1,171 +0,0 @@ -package service - -import ( - "context" - "fmt" - "net" - - "github.com/open-feature/flagd/pkg/eval" - "github.com/open-feature/flagd/pkg/model" - log "github.com/sirupsen/logrus" - gen "go.buf.build/open-feature/flagd-server/open-feature/flagd/schema/v1" - "golang.org/x/sync/errgroup" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/types/known/structpb" -) - -type GRPCServiceConfiguration struct { - Port int32 - ServerKeyPath string - ServerCertPath string - ServerSocketPath string -} - -type GRPCService struct { - GRPCServiceConfiguration *GRPCServiceConfiguration - Eval eval.IEvaluator - gen.UnimplementedServiceServer - Logger *log.Entry -} - -// Serve allows for the use of GRPC only without HTTP, where as HTTP service enables both -// GRPC and HTTP -func (s *GRPCService) Serve(ctx context.Context, eval eval.IEvaluator) error { - var lis net.Listener - var err error - g, gCtx := errgroup.WithContext(ctx) - s.Eval = eval - - // TLS - var serverOpts []grpc.ServerOption - if s.GRPCServiceConfiguration.ServerCertPath != "" && s.GRPCServiceConfiguration.ServerKeyPath != "" { - config, err := loadTLSConfig(s.GRPCServiceConfiguration.ServerCertPath, s.GRPCServiceConfiguration.ServerKeyPath) - if err != nil { - return err - } - serverOpts = append(serverOpts, grpc.Creds(credentials.NewTLS(config))) - } - - grpcServer := grpc.NewServer(serverOpts...) - gen.RegisterServiceServer(grpcServer, s) - - if s.GRPCServiceConfiguration.ServerSocketPath != "" { - lis, err = net.Listen("unix", s.GRPCServiceConfiguration.ServerSocketPath) - } else { - lis, err = net.Listen("tcp", fmt.Sprintf(":%d", s.GRPCServiceConfiguration.Port)) - } - if err != nil { - return err - } - - g.Go(func() error { - return grpcServer.Serve(lis) - }) - <-gCtx.Done() - grpcServer.GracefulStop() - return nil -} - -// TODO: might be able to simplify some of this with generics. -func (s *GRPCService) ResolveBoolean( - ctx context.Context, - req *gen.ResolveBooleanRequest, -) (*gen.ResolveBooleanResponse, error) { - res := gen.ResolveBooleanResponse{} - result, variant, reason, err := s.Eval.ResolveBooleanValue(req.GetFlagKey(), req.GetContext()) - if err != nil { - return &res, s.HandleEvaluationError(err, reason) - } - res.Reason = reason - res.Value = result - res.Variant = variant - return &res, nil -} - -func (s *GRPCService) ResolveString( - ctx context.Context, - req *gen.ResolveStringRequest, -) (*gen.ResolveStringResponse, error) { - res := gen.ResolveStringResponse{} - result, variant, reason, err := s.Eval.ResolveStringValue(req.GetFlagKey(), req.GetContext()) - if err != nil { - return &res, s.HandleEvaluationError(err, reason) - } - res.Reason = reason - res.Value = result - res.Variant = variant - return &res, nil -} - -func (s *GRPCService) ResolveInt( - ctx context.Context, - req *gen.ResolveIntRequest, -) (*gen.ResolveIntResponse, error) { - res := gen.ResolveIntResponse{} - result, variant, reason, err := s.Eval.ResolveIntValue(req.GetFlagKey(), req.GetContext()) - if err != nil { - return &res, s.HandleEvaluationError(err, reason) - } - res.Reason = reason - res.Value = result - res.Variant = variant - return &res, nil -} - -func (s *GRPCService) ResolveFloat( - ctx context.Context, - req *gen.ResolveFloatRequest, -) (*gen.ResolveFloatResponse, error) { - res := gen.ResolveFloatResponse{} - result, variant, reason, err := s.Eval.ResolveFloatValue(req.GetFlagKey(), req.GetContext()) - if err != nil { - return &res, s.HandleEvaluationError(err, reason) - } - res.Reason = reason - res.Value = result - res.Variant = variant - return &res, nil -} - -func (s *GRPCService) ResolveObject( - ctx context.Context, - req *gen.ResolveObjectRequest, -) (*gen.ResolveObjectResponse, error) { - res := gen.ResolveObjectResponse{} - result, variant, reason, err := s.Eval.ResolveObjectValue(req.GetFlagKey(), req.GetContext()) - if err != nil { - return &res, s.HandleEvaluationError(err, reason) - } - val, err := structpb.NewStruct(result) - if err != nil { - return &res, s.HandleEvaluationError(err, reason) - } - res.Reason = reason - res.Value = val - res.Variant = variant - return &res, nil -} - -func (s *GRPCService) HandleEvaluationError(err error, reason string) error { - statusCode := codes.Internal - message := err.Error() - switch message { - case model.FlagNotFoundErrorCode: - statusCode = codes.NotFound - case model.TypeMismatchErrorCode: - statusCode = codes.InvalidArgument - } - st := status.New(statusCode, message) - stWD, err := st.WithDetails(&gen.ErrorResponse{ - ErrorCode: message, - Reason: "ERROR", - }) - if err != nil { - s.Logger.Error(err) - return st.Err() - } - return stWD.Err() -} diff --git a/pkg/service/http_service.go b/pkg/service/http_service.go deleted file mode 100644 index c12d89ca5..000000000 --- a/pkg/service/http_service.go +++ /dev/null @@ -1,194 +0,0 @@ -package service - -import ( - "context" - "crypto/tls" - "encoding/json" - "errors" - "fmt" - "net" - "net/http" - "time" - - "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" - "github.com/open-feature/flagd/pkg/eval" - log "github.com/sirupsen/logrus" - "github.com/soheilhy/cmux" - gen "go.buf.build/open-feature/flagd-server/open-feature/flagd/schema/v1" - "golang.org/x/sync/errgroup" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/status" -) - -type HTTPServiceConfiguration struct { - Port int32 - ServerCertPath string - ServerKeyPath string - ServerSocketPath string -} - -type HTTPService struct { - HTTPServiceConfiguration *HTTPServiceConfiguration - GRPCService *GRPCService - Logger *log.Entry -} - -func (s *HTTPService) tlsListener(l net.Listener) net.Listener { - // Load TLS config - config, err := loadTLSConfig(s.HTTPServiceConfiguration.ServerCertPath, - s.HTTPServiceConfiguration.ServerKeyPath) - if err != nil { - log.Fatal(err) - } - - tlsl := tls.NewListener(l, config) - return tlsl -} - -func (s *HTTPService) ServerGRPC(ctx context.Context, mux *runtime.ServeMux) *grpc.Server { - var address string - var dialOpts []grpc.DialOption - var err error - // handle cert - if s.HTTPServiceConfiguration.ServerCertPath != "" && s.HTTPServiceConfiguration.ServerKeyPath != "" { - tlsCreds, err := loadTLSCredentials(s.HTTPServiceConfiguration.ServerCertPath, - s.HTTPServiceConfiguration.ServerKeyPath) - if err != nil { - log.Fatal(err) - } - dialOpts = append(dialOpts, grpc.WithTransportCredentials(tlsCreds)) - } else { - dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials())) - } - // handle unix socket - if s.HTTPServiceConfiguration.ServerSocketPath != "" { - address = s.HTTPServiceConfiguration.ServerSocketPath - dialOpts = append(dialOpts, grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) { - return net.Dial("unix", addr) - })) - } else { - address = net.JoinHostPort("localhost", fmt.Sprintf("%d", s.HTTPServiceConfiguration.Port)) - } - grpcServer := grpc.NewServer() - gen.RegisterServiceServer(grpcServer, s.GRPCService) - err = gen.RegisterServiceHandlerFromEndpoint( - ctx, - mux, - address, - dialOpts, - ) - if err != nil { - log.Fatal(err) - } - return grpcServer -} - -func (s *HTTPService) ServeHTTP(mux *runtime.ServeMux) *http.Server { - server := &http.Server{ - Handler: mux, - ReadHeaderTimeout: 60 * time.Second, - } - - return server -} - -func (s *HTTPService) Serve(ctx context.Context, eval eval.IEvaluator) error { - s.GRPCService.Eval = eval - g, gCtx := errgroup.WithContext(ctx) - // Mux Setup - mux := runtime.NewServeMux( - runtime.WithErrorHandler(s.HTTPErrorHandler), - ) - // GRPC Setup - grpcServer := s.ServerGRPC(ctx, mux) - // HTTP Setup - httpServer := s.ServeHTTP(mux) - // Net listener - l, err := net.Listen("tcp", fmt.Sprintf(":%d", s.HTTPServiceConfiguration.Port)) - if err != nil { - return err - } - tcpm := cmux.New(l) - // We first match on HTTP 1.1 methods. - httpl := tcpm.Match(cmux.HTTP1Fast()) - // If not matched, we assume that its TLS. - tlsl := tcpm.Match(cmux.Any()) - if s.HTTPServiceConfiguration.ServerCertPath != "" && s.HTTPServiceConfiguration.ServerKeyPath != "" { - tlsl = s.tlsListener(tlsl) - } - // Now, we build another mux recursively to match HTTPS and GoRPC. - tlsm := cmux.New(tlsl) - httpsl := tlsm.Match(cmux.HTTP1Fast()) - var gorpcl net.Listener - if s.HTTPServiceConfiguration.ServerSocketPath != "" { - gorpcl, err = net.Listen("unix", s.HTTPServiceConfiguration.ServerSocketPath) - if err != nil { - return err - } - } else { - gorpcl = tlsm.Match(cmux.Any()) - } - g.Go(func() error { - return httpServer.Serve(httpl) // HTTP - }) - g.Go(func() error { - return httpServer.Serve(httpsl) // HTTPS - }) - g.Go(func() error { - return grpcServer.Serve(gorpcl) // GRPC - }) - g.Go(func() error { - return tlsm.Serve() - }) - g.Go(func() error { - return tcpm.Serve() - }) - <-gCtx.Done() - grpcServer.GracefulStop() - if err = httpServer.Shutdown(context.Background()); err != nil { - return err - } - err = g.Wait() - if err != nil && !errors.Is(err, grpc.ErrServerStopped) && !errors.Is(err, http.ErrServerClosed) { - return err - } - return nil -} - -func (s HTTPService) HTTPErrorHandler( - ctx context.Context, - m *runtime.ServeMux, - ma runtime.Marshaler, - w http.ResponseWriter, - r *http.Request, - err error, -) { - st := status.Convert(err) - switch { - case st.Code() == codes.Unknown: - w.WriteHeader(http.StatusInternalServerError) - case st.Code() == codes.InvalidArgument: - w.WriteHeader(http.StatusBadRequest) - case st.Code() == codes.NotFound: - w.WriteHeader(http.StatusNotFound) - default: - w.WriteHeader(http.StatusInternalServerError) - } - details := st.Details() - if len(details) != 1 { - log.Error(err) - log.Errorf("malformed error received by error handler, details received: %d - %v", len(details), details) - return - } - var res []byte - if res, err = json.Marshal(details[0]); err != nil { - log.Error(err) - return - } - if _, err = w.Write(res); err != nil { - log.Error(err) - return - } -} From 2142ad6d175af9ac3ed2f0287d6dfb5f3c078737 Mon Sep 17 00:00:00 2001 From: James-Milligan Date: Tue, 20 Sep 2022 12:27:22 +0100 Subject: [PATCH 04/21] error handling and readme Signed-off-by: James-Milligan --- README.md | 24 ++++++------ pkg/eval/ievaluator.go | 1 - pkg/model/reason.go | 2 + pkg/service/connect_service.go | 31 ++++++++++++--- pkg/service/connect_service_test.go | 61 +++++++++++++++++++---------- 5 files changed, 80 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 073b3dd3c..e6a966a1b 100644 --- a/README.md +++ b/README.md @@ -36,13 +36,13 @@ This now provides an accessible http or [https](#https) endpoint for flag evalua Command: ```sh -curl -X POST "localhost:8013/flags/myBoolFlag/resolve/boolean" +curl -X POST "localhost:8013/schema.v1.Service/ResolveBoolean" -d '{"flagKey":"myBoolFlag","context":{}}' -H "Content-Type: application/json" ``` Result: ```sh -{"value":true,"reason":"STATIC","variant":"on"} +{"value":true,"reason":"TARGETING_MATCH","variant":"on"} ```
@@ -52,7 +52,7 @@ Result: Command: ```sh -curl -X POST "localhost:8013/flags/myStringFlag/resolve/string" +curl -X POST "localhost:8013/schema.v1.Service/ResolveString" -d '{"flagKey":"myStringFlag","context":{}}' -H "Content-Type: application/json" ``` Result: @@ -68,7 +68,7 @@ Result: Command: ```sh -curl -X POST "localhost:8013/flags/myIntFlag/resolve/int" +curl -X POST "localhost:8013/schema.v1.Service/ResolveInt" -d '{"flagKey":"myIntFlag","context":{}}' -H "Content-Type: application/json" ``` Result: @@ -86,7 +86,7 @@ Result: Command: ```sh -curl -X POST "localhost:8013/flags/myFloatFlag/resolve/float" +curl -X POST "localhost:8013/schema.v1.Service/ResolveFloat" -d '{"flagKey":"myFloatFlag","context":{}}' -H "Content-Type: application/json" ``` Result: @@ -102,7 +102,7 @@ Result: Command: ```sh -curl -X POST "localhost:8013/flags/myObjectFlag/resolve/object" +curl -X POST "localhost:8013/schema.v1.Service/ResolveObject" -d '{"flagKey":"myObjectFlag","context":{}}' -H "Content-Type: application/json" ``` Result: @@ -118,7 +118,7 @@ Result: Command: ```sh -curl -X POST "localhost:8013/flags/isColorYellow/resolve/boolean" -d '{"color": "yellow"}' +curl -X POST "localhost:8013/schema.v1.Service/ResolveBoolean" -d '{"flagKey":"isColorYellow","context":{"color":"yellow"}}' -H "Content-Type: application/json" ``` Result: @@ -136,13 +136,13 @@ A type mismatch error is returned when the resolved value of a flag does not mat Command: ```sh -curl -X POST "localhost:8013/flags/myBoolFlag/resolve/string" +curl -X POST "localhost:8013/schema.v1.Service/ResolveString" -d '{"flagKey":"myBoolFlag","context":{}}' -H "Content-Type: application/json" ``` Result: ```sh -{"error_code":"TYPE_MISMATCH","reason":"ERROR"} +{"code":"invalid_argument","message":"TYPE_MISMATCH"} ```
@@ -154,13 +154,13 @@ The flag not found error is returned when flag key in the request doesn't match Command: ```sh -curl -X POST "localhost:8013/flags/aMissingFlag/resolve/string" +curl -X POST "localhost:8013/schema.v1.Service/ResolveBoolean" -d '{"flagKey":"aMissingFlag","context":{}}' -H "Content-Type: application/json" ``` Result: ```sh -{"error_code":"FLAG_NOT_FOUND","reason":"ERROR"} +{"code":"not_found","message":"FLAG_NOT_FOUND"} ``` ### https @@ -243,7 +243,7 @@ returns whereas ```shell -curl -X POST "localhost:8013/flags/isColorYellow/resolve/boolean" -d '{"color": "white"}' +curl -X POST "localhost:8013/schema.v1.Service/ResolveBoolean" -d '{"flagKey":"isColorYellow","context":{"color":"white"}}' -H "Content-Type: application/json" ``` returns diff --git a/pkg/eval/ievaluator.go b/pkg/eval/ievaluator.go index a75118962..3be3cfb7b 100644 --- a/pkg/eval/ievaluator.go +++ b/pkg/eval/ievaluator.go @@ -8,7 +8,6 @@ import ( IEvaluator implementations store the state of the flags, do parsing and validation of the flag state and evaluate flags in response to handlers. */ - type IEvaluator interface { GetState() (string, error) SetState(state string) error diff --git a/pkg/model/reason.go b/pkg/model/reason.go index 852e75a1f..40f555f7c 100644 --- a/pkg/model/reason.go +++ b/pkg/model/reason.go @@ -1,5 +1,7 @@ package model +type EvaluationReason string + const ( TargetingMatchReason = "TARGETING_MATCH" SplitReason = "SPLIT" diff --git a/pkg/service/connect_service.go b/pkg/service/connect_service.go index f2a4aab06..fdc61a908 100644 --- a/pkg/service/connect_service.go +++ b/pkg/service/connect_service.go @@ -9,6 +9,7 @@ import ( "github.com/bufbuild/connect-go" "github.com/open-feature/flagd/pkg/eval" + "github.com/open-feature/flagd/pkg/model" "github.com/rs/cors" log "github.com/sirupsen/logrus" schemaV1 "go.buf.build/open-feature/flagd-connect/open-feature/flagd/schema/v1" @@ -39,8 +40,8 @@ func (s *ConnectService) Serve(ctx context.Context, eval eval.IEvaluator) error s.Eval = eval mux := http.NewServeMux() - // sockets + // sockets if s.ConnectServiceConfiguration.ServerSocketPath != "" { lis, err = net.Listen("unix", s.ConnectServiceConfiguration.ServerSocketPath) } else { @@ -51,6 +52,7 @@ func (s *ConnectService) Serve(ctx context.Context, eval eval.IEvaluator) error return err } // TLS + path, handler := schemaConnectV1.NewServiceHandler(s) mux.Handle(path, handler) @@ -101,7 +103,8 @@ func (s *ConnectService) ResolveBoolean( result, variant, reason, err := s.Eval.ResolveBooleanValue(req.Msg.GetFlagKey(), req.Msg.GetContext()) if err != nil { log.Error(err) - return res, err + res.Msg.Reason = model.ErrorReason + return res, errFormat(err) } res.Msg.Reason = reason res.Msg.Value = result @@ -117,7 +120,8 @@ func (s *ConnectService) ResolveString( result, variant, reason, err := s.Eval.ResolveStringValue(req.Msg.GetFlagKey(), req.Msg.GetContext()) if err != nil { log.Error(err) - return res, err + res.Msg.Reason = model.ErrorReason + return res, errFormat(err) } res.Msg.Reason = reason res.Msg.Value = result @@ -133,7 +137,8 @@ func (s *ConnectService) ResolveInt( result, variant, reason, err := s.Eval.ResolveIntValue(req.Msg.GetFlagKey(), req.Msg.GetContext()) if err != nil { log.Error(err) - return res, err + res.Msg.Reason = model.ErrorReason + return res, errFormat(err) } res.Msg.Reason = reason res.Msg.Value = result @@ -149,7 +154,8 @@ func (s *ConnectService) ResolveFloat( result, variant, reason, err := s.Eval.ResolveFloatValue(req.Msg.GetFlagKey(), req.Msg.GetContext()) if err != nil { log.Error(err) - return res, err + res.Msg.Reason = model.ErrorReason + return res, errFormat(err) } res.Msg.Reason = reason res.Msg.Value = result @@ -165,7 +171,8 @@ func (s *ConnectService) ResolveObject( result, variant, reason, err := s.Eval.ResolveObjectValue(req.Msg.GetFlagKey(), req.Msg.GetContext()) if err != nil { log.Error(err) - return res, err + res.Msg.Reason = model.ErrorReason + return res, errFormat(err) } val, err := structpb.NewStruct(result) if err != nil { @@ -210,3 +217,15 @@ func newCORS() *cors.Cors { }, }) } + +func errFormat(err error) error { + switch err.Error() { + case model.FlagNotFoundErrorCode: + return connect.NewError(connect.CodeNotFound, err) + case model.TypeMismatchErrorCode: + return connect.NewError(connect.CodeInvalidArgument, err) + case model.ParseErrorCode: + return connect.NewError(connect.CodeInvalidArgument, err) + } + return err +} diff --git a/pkg/service/connect_service_test.go b/pkg/service/connect_service_test.go index ec5742d99..34ece5cc9 100644 --- a/pkg/service/connect_service_test.go +++ b/pkg/service/connect_service_test.go @@ -10,6 +10,7 @@ import ( "github.com/bufbuild/connect-go" "github.com/golang/mock/gomock" + "github.com/open-feature/flagd/pkg/model" service "github.com/open-feature/flagd/pkg/service" log "github.com/sirupsen/logrus" gen "go.buf.build/open-feature/flagd-connect/open-feature/flagd/schema/v1" @@ -148,7 +149,7 @@ func TestConnectService_ResolveBoolean(t *testing.T) { evalFields: resolveBooleanEvalFields{ result: true, variant: ":(", - reason: "ERROR", + reason: model.ErrorReason, }, functionArgs: resolveBooleanFunctionArgs{ context.Background(), @@ -157,7 +158,9 @@ func TestConnectService_ResolveBoolean(t *testing.T) { Context: &structpb.Struct{}, }, }, - want: &gen.ResolveBooleanResponse{}, + want: &gen.ResolveBooleanResponse{ + Reason: model.ErrorReason, + }, wantErr: errors.New("eval interface error"), }, } @@ -212,7 +215,7 @@ func BenchmarkConnectService_ResolveBoolean(b *testing.B) { evalFields: resolveBooleanEvalFields{ result: true, variant: ":(", - reason: "ERROR", + reason: model.ErrorReason, }, functionArgs: resolveBooleanFunctionArgs{ context.Background(), @@ -221,7 +224,9 @@ func BenchmarkConnectService_ResolveBoolean(b *testing.B) { Context: &structpb.Struct{}, }, }, - want: &gen.ResolveBooleanResponse{}, + want: &gen.ResolveBooleanResponse{ + Reason: model.ErrorReason, + }, wantErr: errors.New("eval interface error"), }, } @@ -294,7 +299,7 @@ func TestConnectService_ResolveString(t *testing.T) { evalFields: resolveStringEvalFields{ result: "true", variant: ":(", - reason: "ERROR", + reason: model.ErrorReason, }, functionArgs: resolveStringFunctionArgs{ context.Background(), @@ -303,7 +308,9 @@ func TestConnectService_ResolveString(t *testing.T) { Context: &structpb.Struct{}, }, }, - want: &gen.ResolveStringResponse{}, + want: &gen.ResolveStringResponse{ + Reason: model.ErrorReason, + }, wantErr: errors.New("eval interface error"), }, } @@ -358,7 +365,7 @@ func BenchmarkConnectService_ResolveString(b *testing.B) { evalFields: resolveStringEvalFields{ result: "true", variant: ":(", - reason: "ERROR", + reason: model.ErrorReason, }, functionArgs: resolveStringFunctionArgs{ context.Background(), @@ -367,7 +374,9 @@ func BenchmarkConnectService_ResolveString(b *testing.B) { Context: &structpb.Struct{}, }, }, - want: &gen.ResolveStringResponse{}, + want: &gen.ResolveStringResponse{ + Reason: model.ErrorReason, + }, wantErr: errors.New("eval interface error"), }, } @@ -440,7 +449,7 @@ func TestConnectService_ResolveFloat(t *testing.T) { evalFields: resolveFloatEvalFields{ result: 12, variant: ":(", - reason: "ERROR", + reason: model.ErrorReason, }, functionArgs: resolveFloatFunctionArgs{ context.Background(), @@ -449,7 +458,9 @@ func TestConnectService_ResolveFloat(t *testing.T) { Context: &structpb.Struct{}, }, }, - want: &gen.ResolveFloatResponse{}, + want: &gen.ResolveFloatResponse{ + Reason: model.ErrorReason, + }, wantErr: errors.New("eval interface error"), }, } @@ -504,7 +515,7 @@ func BenchmarkConnectService_ResolveFloat(b *testing.B) { evalFields: resolveFloatEvalFields{ result: 12, variant: ":(", - reason: "ERROR", + reason: model.ErrorReason, }, functionArgs: resolveFloatFunctionArgs{ context.Background(), @@ -513,7 +524,9 @@ func BenchmarkConnectService_ResolveFloat(b *testing.B) { Context: &structpb.Struct{}, }, }, - want: &gen.ResolveFloatResponse{}, + want: &gen.ResolveFloatResponse{ + Reason: model.ErrorReason, + }, wantErr: errors.New("eval interface error"), }, } @@ -586,7 +599,7 @@ func TestConnectService_ResolveInt(t *testing.T) { evalFields: resolveIntEvalFields{ result: 12, variant: ":(", - reason: "ERROR", + reason: model.ErrorReason, }, functionArgs: resolveIntFunctionArgs{ context.Background(), @@ -595,7 +608,9 @@ func TestConnectService_ResolveInt(t *testing.T) { Context: &structpb.Struct{}, }, }, - want: &gen.ResolveIntResponse{}, + want: &gen.ResolveIntResponse{ + Reason: model.ErrorReason, + }, wantErr: errors.New("eval interface error"), }, } @@ -650,7 +665,7 @@ func BenchmarkConnectService_ResolveInt(b *testing.B) { evalFields: resolveIntEvalFields{ result: 12, variant: ":(", - reason: "ERROR", + reason: model.ErrorReason, }, functionArgs: resolveIntFunctionArgs{ context.Background(), @@ -659,7 +674,9 @@ func BenchmarkConnectService_ResolveInt(b *testing.B) { Context: &structpb.Struct{}, }, }, - want: &gen.ResolveIntResponse{}, + want: &gen.ResolveIntResponse{ + Reason: model.ErrorReason, + }, wantErr: errors.New("eval interface error"), }, } @@ -736,7 +753,7 @@ func TestConnectService_ResolveObject(t *testing.T) { "food": "bars", }, variant: ":(", - reason: "ERROR", + reason: model.ErrorReason, }, functionArgs: resolveObjectFunctionArgs{ context.Background(), @@ -745,7 +762,9 @@ func TestConnectService_ResolveObject(t *testing.T) { Context: &structpb.Struct{}, }, }, - want: &gen.ResolveObjectResponse{}, + want: &gen.ResolveObjectResponse{ + Reason: model.ErrorReason, + }, wantErr: errors.New("eval interface error"), }, } @@ -812,7 +831,7 @@ func BenchmarkConnectService_ResolveObject(b *testing.B) { "food": "bars", }, variant: ":(", - reason: "ERROR", + reason: model.ErrorReason, }, functionArgs: resolveObjectFunctionArgs{ context.Background(), @@ -821,7 +840,9 @@ func BenchmarkConnectService_ResolveObject(b *testing.B) { Context: &structpb.Struct{}, }, }, - want: &gen.ResolveObjectResponse{}, + want: &gen.ResolveObjectResponse{ + Reason: model.ErrorReason, + }, wantErr: errors.New("eval interface error"), }, } From a84f4a041cf752f78d88bc3af3cb9479495833d5 Mon Sep 17 00:00:00 2001 From: James-Milligan Date: Tue, 20 Sep 2022 12:55:31 +0100 Subject: [PATCH 05/21] updated all examples Signed-off-by: James-Milligan --- docs/configuration.md | 1 - docs/fractional_evaluation.md | 4 ++-- docs/http_int_response.md | 6 +++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 46cc1e6fe..9839de291 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -12,7 +12,6 @@ Supported flags are as follows (result of running `./flagd start --help`): -p, --port int32 Port to listen on (default 8013) -c, --server-cert-path string Server side tls certificate path -k, --server-key-path string Server side tls key path - -s, --service-provider string Set a service provider e.g. http or grpc (default "http") -d, --socket-path string Set the flagd socket path. With grpc the service will become available on this address. With http(s) the grpc-gateway proxy will use this address internally -y, --sync-provider string Set a sync provider e.g. filepath or remote (default "filepath") -f, --uri strings Set a sync provider uri to read data from this can be a filepath or url. Using multiple providers is supported where collisions between flags with the same key, the later will be used. diff --git a/docs/fractional_evaluation.md b/docs/fractional_evaluation.md index 58c11d0bb..b6cc49e68 100644 --- a/docs/fractional_evaluation.md +++ b/docs/fractional_evaluation.md @@ -45,9 +45,9 @@ Flags defined as such: will return variant `red` 50% of the time, `blue` 20% of the time & `green` 30% of the time. ```shell -$ curl -X POST "localhost:8013/flags/headerColor/resolve/string" -d '{"email": "foo@bar.com"}' +$ curl -X POST "localhost:8013/schema.v1.Service/ResolveString" -d '{"email": "foo@bar.com"}' -H "Content-Type: application/json" {"value":"#0000FF","reason":"TARGETING_MATCH","variant":"blue"}% -$ curl -X POST "localhost:8013/flags/headerColor/resolve/string" -d '{"email": "foo@test.com"}' +$ curl -X POST "localhost:8013/schema.v1.Service/ResolveString" -d '{"email": "foo@test.com"}' -H "Content-Type: application/json" {"value":"#00FF00","reason":"TARGETING_MATCH","variant":"green"}% ``` diff --git a/docs/http_int_response.md b/docs/http_int_response.md index 5872f7795..ab64f6e34 100644 --- a/docs/http_int_response.md +++ b/docs/http_int_response.md @@ -4,7 +4,7 @@ Why is my `int` response a `string`? Command: ```sh -curl -X POST "localhost:8013/flags/myIntFlag/resolve/int" +curl -X POST "localhost:8013/schema.v1.Service/ResolveInt" -d '{"flagKey":"myIntFlag","context":{}}' -H "Content-Type: application/json" ``` Result: ```sh @@ -14,9 +14,9 @@ When interacting directly with the flagD http(s) api and requesting an `int` the
Command: ```sh -curl -X POST "localhost:8013/flags/myIntFlag/resolve/float" +curl -X POST "localhost:8013/schema.v1.Service/ResolveFloat" -d '{"flagKey":"myIntFlag","context":{}}' -H "Content-Type: application/json" ``` Result: ```sh -{"value":1.23,"reason":"STATIC","variant":"one"} +{"value":1,"reason":"STATIC","variant":"one"} ``` From e02089d9356d5b1d6317f7b1cb140b276ea46c97 Mon Sep 17 00:00:00 2001 From: James-Milligan Date: Tue, 20 Sep 2022 13:08:45 +0100 Subject: [PATCH 06/21] updated examples Signed-off-by: James-Milligan --- docs/fractional_evaluation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/fractional_evaluation.md b/docs/fractional_evaluation.md index b6cc49e68..496f5bc6b 100644 --- a/docs/fractional_evaluation.md +++ b/docs/fractional_evaluation.md @@ -45,9 +45,9 @@ Flags defined as such: will return variant `red` 50% of the time, `blue` 20% of the time & `green` 30% of the time. ```shell -$ curl -X POST "localhost:8013/schema.v1.Service/ResolveString" -d '{"email": "foo@bar.com"}' -H "Content-Type: application/json" +$ curl -X POST "localhost:8013/schema.v1.Service/ResolveString" -d ''{"flagKey":"myFloatFlag","context":{"email": "foo@bar.com"}}'' -H "Content-Type: application/json" {"value":"#0000FF","reason":"TARGETING_MATCH","variant":"blue"}% -$ curl -X POST "localhost:8013/schema.v1.Service/ResolveString" -d '{"email": "foo@test.com"}' -H "Content-Type: application/json" +$ curl -X POST "localhost:8013/schema.v1.Service/ResolveString" -d ''{"flagKey":"myFloatFlag","context":{"email": "foo@test.com"}}'' -H "Content-Type: application/json" {"value":"#00FF00","reason":"TARGETING_MATCH","variant":"green"}% ``` From dd370c7e0e8e16c32406100d9d100ca8d0c5fffa Mon Sep 17 00:00:00 2001 From: James-Milligan Date: Tue, 20 Sep 2022 13:15:32 +0100 Subject: [PATCH 07/21] merge Signed-off-by: James-Milligan --- cmd/start.go | 2 +- docs/fractional_evaluation.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/start.go b/cmd/start.go index 182c72e7d..35528b40e 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -14,7 +14,7 @@ const ( portFlagName = "port" socketPathFlagName = "socket-path" syncProviderFlagName = "sync-provider" - providerArgsFlagName = "sync-provider-args" + providerArgsFlagName = "sync-provider-args" evaluatorFlagName = "evaluator" serverCertPathFlagName = "server-cert-path" serverKeyPathFlagName = "server-key-path" diff --git a/docs/fractional_evaluation.md b/docs/fractional_evaluation.md index 496f5bc6b..75ec3d412 100644 --- a/docs/fractional_evaluation.md +++ b/docs/fractional_evaluation.md @@ -45,9 +45,9 @@ Flags defined as such: will return variant `red` 50% of the time, `blue` 20% of the time & `green` 30% of the time. ```shell -$ curl -X POST "localhost:8013/schema.v1.Service/ResolveString" -d ''{"flagKey":"myFloatFlag","context":{"email": "foo@bar.com"}}'' -H "Content-Type: application/json" +$ curl -X POST "localhost:8013/schema.v1.Service/ResolveString" -d ''{"flagKey":"headerColor","context":{"email": "foo@bar.com"}}'' -H "Content-Type: application/json" {"value":"#0000FF","reason":"TARGETING_MATCH","variant":"blue"}% -$ curl -X POST "localhost:8013/schema.v1.Service/ResolveString" -d ''{"flagKey":"myFloatFlag","context":{"email": "foo@test.com"}}'' -H "Content-Type: application/json" +$ curl -X POST "localhost:8013/schema.v1.Service/ResolveString" -d ''{"flagKey":"headerColor","context":{"email": "foo@test.com"}}'' -H "Content-Type: application/json" {"value":"#00FF00","reason":"TARGETING_MATCH","variant":"green"}% ``` From 12e4de3b3a1da4492a723c0a2f50ea0b85e05804 Mon Sep 17 00:00:00 2001 From: James-Milligan Date: Tue, 20 Sep 2022 15:08:01 +0100 Subject: [PATCH 08/21] linting fixes Signed-off-by: James-Milligan --- pkg/service/connect_service.go | 21 ++++++++++----------- pkg/service/utils.go | 34 ---------------------------------- 2 files changed, 10 insertions(+), 45 deletions(-) delete mode 100644 pkg/service/utils.go diff --git a/pkg/service/connect_service.go b/pkg/service/connect_service.go index fdc61a908..afae9243b 100644 --- a/pkg/service/connect_service.go +++ b/pkg/service/connect_service.go @@ -6,6 +6,7 @@ import ( "fmt" "net" "net/http" + "time" "github.com/bufbuild/connect-go" "github.com/open-feature/flagd/pkg/eval" @@ -35,9 +36,7 @@ func (s *ConnectService) Serve(ctx context.Context, eval eval.IEvaluator) error var handler http.Handler var lis net.Listener var err error - tls := false - s.Eval = eval mux := http.NewServeMux() @@ -51,11 +50,9 @@ func (s *ConnectService) Serve(ctx context.Context, eval eval.IEvaluator) error if err != nil { return err } - // TLS - path, handler := schemaConnectV1.NewServiceHandler(s) mux.Handle(path, handler) - + // TLS if s.ConnectServiceConfiguration.ServerCertPath != "" && s.ConnectServiceConfiguration.ServerKeyPath != "" { tls = true handler = newCORS().Handler(mux) @@ -65,29 +62,31 @@ func (s *ConnectService) Serve(ctx context.Context, eval eval.IEvaluator) error &http2.Server{}, ) } - + srv := http.Server{ + ReadTimeout: 2 * time.Second, + WriteTimeout: 4 * time.Second, + ReadHeaderTimeout: time.Second, + Handler: handler, + } errChan := make(chan error, 1) go func() { if tls { - if err := http.ServeTLS( + if err := srv.ServeTLS( lis, - handler, s.ConnectServiceConfiguration.ServerCertPath, s.ConnectServiceConfiguration.ServerKeyPath, ); err != nil && !errors.Is(err, http.ErrServerClosed) { errChan <- err } } else { - if err := http.Serve( + if err := srv.Serve( lis, - handler, ); err != nil && !errors.Is(err, http.ErrServerClosed) { errChan <- err } } close(errChan) }() - <-ctx.Done() if err := lis.Close(); err != nil { return err diff --git a/pkg/service/utils.go b/pkg/service/utils.go deleted file mode 100644 index 78b956ed5..000000000 --- a/pkg/service/utils.go +++ /dev/null @@ -1,34 +0,0 @@ -package service - -import ( - "crypto/rand" - "crypto/tls" - - "google.golang.org/grpc/credentials" -) - -func loadTLSCredentials(serverCertPath string, serverKeyPath string) (credentials.TransportCredentials, error) { - // Load server's certificate and private key - creds, err := credentials.NewServerTLSFromFile(serverCertPath, serverKeyPath) - if err != nil { - return nil, err - } - - return creds, nil -} - -func loadTLSConfig(certPath, keyPath string) (*tls.Config, error) { - certificate, err := tls.LoadX509KeyPair(certPath, - keyPath) - if err != nil { - return nil, err - } - - config := &tls.Config{ - Certificates: []tls.Certificate{certificate}, - Rand: rand.Reader, - MinVersion: tls.VersionTLS12, - } - - return config, nil -} From 740d10daee243d6424f1867547289fcbd9552f96 Mon Sep 17 00:00:00 2001 From: James-Milligan Date: Tue, 20 Sep 2022 16:01:16 +0100 Subject: [PATCH 09/21] removed error from benchmark, reduces logging should be more accurate Signed-off-by: James-Milligan --- pkg/service/connect_service_test.go | 74 ----------------------------- 1 file changed, 74 deletions(-) diff --git a/pkg/service/connect_service_test.go b/pkg/service/connect_service_test.go index 34ece5cc9..9bbc4405e 100644 --- a/pkg/service/connect_service_test.go +++ b/pkg/service/connect_service_test.go @@ -361,24 +361,6 @@ func BenchmarkConnectService_ResolveString(b *testing.B) { }, wantErr: nil, }, - "eval returns error": { - evalFields: resolveStringEvalFields{ - result: "true", - variant: ":(", - reason: model.ErrorReason, - }, - functionArgs: resolveStringFunctionArgs{ - context.Background(), - &gen.ResolveStringRequest{ - FlagKey: "string", - Context: &structpb.Struct{}, - }, - }, - want: &gen.ResolveStringResponse{ - Reason: model.ErrorReason, - }, - wantErr: errors.New("eval interface error"), - }, } for name, tt := range tests { eval := NewMockIEvaluator(ctrl) @@ -511,24 +493,6 @@ func BenchmarkConnectService_ResolveFloat(b *testing.B) { }, wantErr: nil, }, - "eval returns error": { - evalFields: resolveFloatEvalFields{ - result: 12, - variant: ":(", - reason: model.ErrorReason, - }, - functionArgs: resolveFloatFunctionArgs{ - context.Background(), - &gen.ResolveFloatRequest{ - FlagKey: "float", - Context: &structpb.Struct{}, - }, - }, - want: &gen.ResolveFloatResponse{ - Reason: model.ErrorReason, - }, - wantErr: errors.New("eval interface error"), - }, } for name, tt := range tests { eval := NewMockIEvaluator(ctrl) @@ -661,24 +625,6 @@ func BenchmarkConnectService_ResolveInt(b *testing.B) { }, wantErr: nil, }, - "eval returns error": { - evalFields: resolveIntEvalFields{ - result: 12, - variant: ":(", - reason: model.ErrorReason, - }, - functionArgs: resolveIntFunctionArgs{ - context.Background(), - &gen.ResolveIntRequest{ - FlagKey: "int", - Context: &structpb.Struct{}, - }, - }, - want: &gen.ResolveIntResponse{ - Reason: model.ErrorReason, - }, - wantErr: errors.New("eval interface error"), - }, } for name, tt := range tests { eval := NewMockIEvaluator(ctrl) @@ -825,26 +771,6 @@ func BenchmarkConnectService_ResolveObject(b *testing.B) { }, wantErr: nil, }, - "eval returns error": { - evalFields: resolveObjectEvalFields{ - result: map[string]interface{}{ - "food": "bars", - }, - variant: ":(", - reason: model.ErrorReason, - }, - functionArgs: resolveObjectFunctionArgs{ - context.Background(), - &gen.ResolveObjectRequest{ - FlagKey: "object", - Context: &structpb.Struct{}, - }, - }, - want: &gen.ResolveObjectResponse{ - Reason: model.ErrorReason, - }, - wantErr: errors.New("eval interface error"), - }, } for name, tt := range tests { eval := NewMockIEvaluator(ctrl) From dfcb6702b17d5e153a65773a778500ebc77096c4 Mon Sep 17 00:00:00 2001 From: James-Milligan Date: Wed, 21 Sep 2022 10:16:24 +0100 Subject: [PATCH 10/21] removed error route from benchmark Signed-off-by: James-Milligan --- pkg/service/connect_service_test.go | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/pkg/service/connect_service_test.go b/pkg/service/connect_service_test.go index 9bbc4405e..caa1fce6c 100644 --- a/pkg/service/connect_service_test.go +++ b/pkg/service/connect_service_test.go @@ -211,24 +211,6 @@ func BenchmarkConnectService_ResolveBoolean(b *testing.B) { }, wantErr: nil, }, - "eval returns error": { - evalFields: resolveBooleanEvalFields{ - result: true, - variant: ":(", - reason: model.ErrorReason, - }, - functionArgs: resolveBooleanFunctionArgs{ - context.Background(), - &gen.ResolveBooleanRequest{ - FlagKey: "bool", - Context: &structpb.Struct{}, - }, - }, - want: &gen.ResolveBooleanResponse{ - Reason: model.ErrorReason, - }, - wantErr: errors.New("eval interface error"), - }, } for name, tt := range tests { eval := NewMockIEvaluator(ctrl) From 121f050835c9ea04a9f8250a1dfca3acb615ef29 Mon Sep 17 00:00:00 2001 From: James-Milligan Date: Wed, 21 Sep 2022 10:17:01 +0100 Subject: [PATCH 11/21] merge conflict Signed-off-by: James-Milligan --- docs/configuration.md.orig | 50 -------------------------------------- 1 file changed, 50 deletions(-) delete mode 100644 docs/configuration.md.orig diff --git a/docs/configuration.md.orig b/docs/configuration.md.orig deleted file mode 100644 index aa4485daa..000000000 --- a/docs/configuration.md.orig +++ /dev/null @@ -1,50 +0,0 @@ -### Configuration - -`flagd` supports configuration via config file, environment variables and flags. In cases of conflict, flags have the -highest priority, followed by environment variables and finally config file. - -Supported flags are as follows (result of running `./flagd start --help`): - -``` - -b, --bearer-token string Set a bearer token to use for remote sync - -e, --evaluator string Set an evaluator e.g. json (default "json") - -h, --help help for start - -p, --port int32 Port to listen on (default 8013) - -c, --server-cert-path string Server side tls certificate path - -k, --server-key-path string Server side tls key path -<<<<<<< HEAD -======= - -s, --service-provider string Set a service provider e.g. http or grpc (default "http") - -a, --sync-provider-args Sync provider arguments as key values separated by = ->>>>>>> 82278c7cf08cc6b50f49ab500caf6f9003fc0823 - -d, --socket-path string Set the flagd socket path. With grpc the service will become available on this address. With http(s) the grpc-gateway proxy will use this address internally - -y, --sync-provider string Set a sync provider e.g. filepath or remote (default "filepath") - -f, --uri strings Set a sync provider uri to read data from this can be a filepath or url. Using multiple providers is supported where collisions between flags with the same key, the later will be used. -``` - -Environment variable keys are uppercased, prefixed with `FLAGD_` and all `-` are replaced with `_`. For example, -`sync-provider` in environment variable form is `FLAGD_SYNC_PROVIDER`. - -Config file expects the keys to have the exact naming as the flags. - - -### Customising sync providers - -Custom sync providers can be used to provide flag evaluation logic. - -#### Kubernetes provider - -The Kubernetes provider allows flagD to connect to a Kubernetes cluster and evaluate flags against a specified FeatureFlagConfiguration resource as defined within the [open-feature-operator](https://github.com/open-feature/open-feature-operator/blob/main/apis/core/v1alpha1/featureflagconfiguration_types.go) spec. - -To use an existing FeatureFlagConfiguration custom resource, start flagD with the following command: - -```shell -flagd start --sync-provider=kubernetes --sync-provider-args=featureflagconfiguration=my-example --sync-provider-args=namespace=default -``` - -An additional optional flag `refreshtime` can be applied to shorten the cache refresh when using the Kubernetes provider ( The default is 5s ). As an example: - -```shell -flagd start --sync-provider=kubernetes --sync-provider-args=featureflagconfiguration=my-example --sync-provider-args=namespace=default ---sync-provider-args=refreshtime=1s -``` \ No newline at end of file From 7b9aa73cde6b4fc20b8e3961cb0dc38ba8c2436a Mon Sep 17 00:00:00 2001 From: James-Milligan Date: Wed, 21 Sep 2022 12:58:43 +0100 Subject: [PATCH 12/21] update README and docs Signed-off-by: James-Milligan --- README.md | 2 +- docs/configuration.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 45df6abe8..8da87c114 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ curl -X POST "localhost:8013/schema.v1.Service/ResolveBoolean" -d '{"flagKey":"m Result: ```sh -{"value":true,"reason":"TARGETING_MATCH","variant":"on"} +{"value":true,"reason":"STATIC","variant":"on"} ```
diff --git a/docs/configuration.md b/docs/configuration.md index 7b4a65ef2..88d963c5d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -13,7 +13,7 @@ Supported flags are as follows (result of running `./flagd start --help`): -c, --server-cert-path string Server side tls certificate path -k, --server-key-path string Server side tls key path -a, --sync-provider-args Sync provider arguments as key values separated by = - -d, --socket-path string Set the flagd socket path. With grpc the service will become available on this address. With http(s) the grpc-gateway proxy will use this address internally + -d, --socket-path string Set the flagd socket path. -y, --sync-provider string Set a sync provider e.g. filepath or remote (default "filepath") -f, --uri strings Set a sync provider uri to read data from this can be a filepath or url. Using multiple providers is supported where collisions between flags with the same key, the later will be used. ``` From c5edb1864440aba466f0dd1bb18dcfeb5d1f6dbb Mon Sep 17 00:00:00 2001 From: James-Milligan Date: Wed, 21 Sep 2022 15:06:00 +0100 Subject: [PATCH 13/21] added disabled error reason Signed-off-by: James-Milligan --- pkg/service/connect_service.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/service/connect_service.go b/pkg/service/connect_service.go index afae9243b..9f4b54fbf 100644 --- a/pkg/service/connect_service.go +++ b/pkg/service/connect_service.go @@ -214,7 +214,7 @@ func newCORS() *cors.Cors { "Grpc-Status", "Grpc-Status-Details-Bin", }, - }) + }) // add interace type name + comment to say why its liek that } func errFormat(err error) error { @@ -223,6 +223,8 @@ func errFormat(err error) error { return connect.NewError(connect.CodeNotFound, err) case model.TypeMismatchErrorCode: return connect.NewError(connect.CodeInvalidArgument, err) + case model.DisabledReason: + return connect.NewError(connect.CodeUnavailable, err) case model.ParseErrorCode: return connect.NewError(connect.CodeInvalidArgument, err) } From 9631751ec3fb9cc79b62d0bf0248c7e12b692a5a Mon Sep 17 00:00:00 2001 From: James-Milligan Date: Wed, 21 Sep 2022 15:11:15 +0100 Subject: [PATCH 14/21] removed comment line Signed-off-by: James-Milligan --- pkg/service/connect_service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/service/connect_service.go b/pkg/service/connect_service.go index 9f4b54fbf..1d5d1e7f4 100644 --- a/pkg/service/connect_service.go +++ b/pkg/service/connect_service.go @@ -214,7 +214,7 @@ func newCORS() *cors.Cors { "Grpc-Status", "Grpc-Status-Details-Bin", }, - }) // add interace type name + comment to say why its liek that + }) } func errFormat(err error) error { From 72e12235db121c377f041f1cdca42d3970e8301e Mon Sep 17 00:00:00 2001 From: James-Milligan Date: Wed, 21 Sep 2022 15:43:24 +0100 Subject: [PATCH 15/21] updated missed curl requests Signed-off-by: James-Milligan --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8da87c114..5443c9f9f 100644 --- a/README.md +++ b/README.md @@ -172,7 +172,7 @@ When it is desired to use TLS for increased security, flagD can be started with This enables you to use an upgraded connection for the previous example requests, such as the following: ``` -curl -X POST "https://localhost:8013/flags/myBoolFlag/resolve/boolean" +curl -X POST "localhost:8013/schema.v1.Service/ResolveBoolean" -d '{"flagKey":"myBoolFlag","context":{}}' -H "Content-Type: application/json" // {"value":true,"reason":"STATIC","variant":"on"} ``` @@ -231,7 +231,7 @@ A flag is defined as such: The rule provided returns `"on"` if `var color == "yellow"` and `"off"` otherwise: ```shell -curl -X POST "localhost:8013/flags/isColorYellow/resolve/boolean" -d '{"color": "yellow"}' +curl -X POST "localhost:8013/schema.v1.Service/ResolveBoolean" -d '{"flagKey":"isColorYellow","context":{"color":"yellow"}}' -H "Content-Type: application/json" ``` returns From f5fce5e211feb81398c0f0b2b1e96c6bf0b9f43f Mon Sep 17 00:00:00 2001 From: James-Milligan Date: Wed, 21 Sep 2022 18:01:12 +0100 Subject: [PATCH 16/21] add cors flag Signed-off-by: James-Milligan --- cmd/start.go | 5 +++ pkg/runtime/from_config.go | 1 + pkg/runtime/runtime.go | 1 + pkg/service/connect_service.go | 66 +++++++++++++++++++++------------- 4 files changed, 48 insertions(+), 25 deletions(-) diff --git a/cmd/start.go b/cmd/start.go index 35528b40e..a52fc3eb8 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -20,6 +20,7 @@ const ( serverKeyPathFlagName = "server-key-path" uriFlagName = "uri" bearerTokenFlagName = "bearer-token" + corsFlagName = "cors" ) func init() { @@ -47,6 +48,7 @@ func init() { "flags with the same key, the later will be used.") flags.StringP( bearerTokenFlagName, "b", "", "Set a bearer token to use for remote sync") + flags.BoolP(corsFlagName, "C", false, "Set CORS enable all headers on responses") _ = viper.BindPFlag(portFlagName, flags.Lookup(portFlagName)) _ = viper.BindPFlag(socketPathFlagName, flags.Lookup(socketPathFlagName)) @@ -57,6 +59,7 @@ func init() { _ = viper.BindPFlag(serverKeyPathFlagName, flags.Lookup(serverKeyPathFlagName)) _ = viper.BindPFlag(uriFlagName, flags.Lookup(uriFlagName)) _ = viper.BindPFlag(bearerTokenFlagName, flags.Lookup(bearerTokenFlagName)) + _ = viper.BindPFlag(corsFlagName, flags.Lookup(corsFlagName)) } // startCmd represents the start command @@ -86,6 +89,8 @@ var startCmd = &cobra.Command{ SyncBearerToken: viper.GetString(bearerTokenFlagName), Evaluator: viper.GetString(evaluatorFlagName), + + CORS: viper.GetBool(corsFlagName), }) if err != nil { log.Error(err) diff --git a/pkg/runtime/from_config.go b/pkg/runtime/from_config.go index 5860a46d7..af68258be 100644 --- a/pkg/runtime/from_config.go +++ b/pkg/runtime/from_config.go @@ -39,6 +39,7 @@ func (r *Runtime) setService() { ServerKeyPath: r.config.ServiceKeyPath, ServerCertPath: r.config.ServiceCertPath, ServerSocketPath: r.config.ServiceSocketPath, + CORS: r.config.CORS, }, } } diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go index e7d5b18cb..e123f6409 100644 --- a/pkg/runtime/runtime.go +++ b/pkg/runtime/runtime.go @@ -33,6 +33,7 @@ type Config struct { SyncBearerToken string Evaluator string + CORS bool } func (r *Runtime) startSyncer(ctx context.Context, syncr sync.ISync) error { diff --git a/pkg/service/connect_service.go b/pkg/service/connect_service.go index 1d5d1e7f4..464177fac 100644 --- a/pkg/service/connect_service.go +++ b/pkg/service/connect_service.go @@ -23,6 +23,8 @@ import ( type ConnectService struct { Eval eval.IEvaluator ConnectServiceConfiguration *ConnectServiceConfiguration + handler http.Handler + tls bool } type ConnectServiceConfiguration struct { @@ -30,38 +32,22 @@ type ConnectServiceConfiguration struct { ServerCertPath string ServerKeyPath string ServerSocketPath string + CORS bool } func (s *ConnectService) Serve(ctx context.Context, eval eval.IEvaluator) error { - var handler http.Handler - var lis net.Listener - var err error - tls := false s.Eval = eval mux := http.NewServeMux() - // sockets - if s.ConnectServiceConfiguration.ServerSocketPath != "" { - lis, err = net.Listen("unix", s.ConnectServiceConfiguration.ServerSocketPath) - } else { - address := net.JoinHostPort("localhost", fmt.Sprintf("%d", s.ConnectServiceConfiguration.Port)) - lis, err = net.Listen("tcp", address) - } + lis, err := s.buildListener() if err != nil { return err } path, handler := schemaConnectV1.NewServiceHandler(s) - mux.Handle(path, handler) - // TLS - if s.ConnectServiceConfiguration.ServerCertPath != "" && s.ConnectServiceConfiguration.ServerKeyPath != "" { - tls = true - handler = newCORS().Handler(mux) - } else { - handler = h2c.NewHandler( - newCORS().Handler(mux), - &http2.Server{}, - ) - } + s.handler = handler + mux.Handle(path, s.handler) + s.buildHandler(mux) + srv := http.Server{ ReadTimeout: 2 * time.Second, WriteTimeout: 4 * time.Second, @@ -70,7 +56,7 @@ func (s *ConnectService) Serve(ctx context.Context, eval eval.IEvaluator) error } errChan := make(chan error, 1) go func() { - if tls { + if s.tls { if err := srv.ServeTLS( lis, s.ConnectServiceConfiguration.ServerCertPath, @@ -94,6 +80,38 @@ func (s *ConnectService) Serve(ctx context.Context, eval eval.IEvaluator) error return <-errChan } +func (s *ConnectService) buildHandler(mux *http.ServeMux) { + if s.ConnectServiceConfiguration.ServerCertPath != "" && s.ConnectServiceConfiguration.ServerKeyPath != "" { + s.tls = true + if s.ConnectServiceConfiguration.CORS { + s.handler = newCORS().Handler(mux) + } else { + s.handler = mux + } + } else { + if s.ConnectServiceConfiguration.CORS { + s.handler = h2c.NewHandler( + newCORS().Handler(mux), + &http2.Server{}, + ) + } else { + h2c.NewHandler( + newCORS().Handler(mux), + &http2.Server{}, + ) + } + } +} + +func (s *ConnectService) buildListener() (net.Listener, error) { + if s.ConnectServiceConfiguration.ServerSocketPath != "" { + return net.Listen("unix", s.ConnectServiceConfiguration.ServerSocketPath) + } else { + address := net.JoinHostPort("localhost", fmt.Sprintf("%d", s.ConnectServiceConfiguration.Port)) + return net.Listen("tcp", address) + } +} + func (s *ConnectService) ResolveBoolean( ctx context.Context, req *connect.Request[schemaV1.ResolveBooleanRequest], @@ -184,8 +202,6 @@ func (s *ConnectService) ResolveObject( } func newCORS() *cors.Cors { - // To let web developers play with the demo ConnectService from browsers, we need a - // very permissive CORS setup. return cors.New(cors.Options{ AllowedMethods: []string{ http.MethodHead, From 85598d499d2ed4b7ae33558ab131b1d225ea4e65 Mon Sep 17 00:00:00 2001 From: James-Milligan Date: Wed, 21 Sep 2022 18:04:10 +0100 Subject: [PATCH 17/21] added data loss code for parse error Signed-off-by: James-Milligan --- pkg/service/connect_service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/service/connect_service.go b/pkg/service/connect_service.go index 464177fac..ee5b37335 100644 --- a/pkg/service/connect_service.go +++ b/pkg/service/connect_service.go @@ -242,7 +242,7 @@ func errFormat(err error) error { case model.DisabledReason: return connect.NewError(connect.CodeUnavailable, err) case model.ParseErrorCode: - return connect.NewError(connect.CodeInvalidArgument, err) + return connect.NewError(connect.CodeDataLoss, err) } return err } From b57a7c70299733e319f60e8ed9b47e3ba3d768c2 Mon Sep 17 00:00:00 2001 From: James-Milligan Date: Wed, 21 Sep 2022 18:15:32 +0100 Subject: [PATCH 18/21] lint fix Signed-off-by: James-Milligan --- pkg/service/connect_service.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/service/connect_service.go b/pkg/service/connect_service.go index ee5b37335..17e20286f 100644 --- a/pkg/service/connect_service.go +++ b/pkg/service/connect_service.go @@ -106,10 +106,9 @@ func (s *ConnectService) buildHandler(mux *http.ServeMux) { func (s *ConnectService) buildListener() (net.Listener, error) { if s.ConnectServiceConfiguration.ServerSocketPath != "" { return net.Listen("unix", s.ConnectServiceConfiguration.ServerSocketPath) - } else { - address := net.JoinHostPort("localhost", fmt.Sprintf("%d", s.ConnectServiceConfiguration.Port)) - return net.Listen("tcp", address) } + address := net.JoinHostPort("localhost", fmt.Sprintf("%d", s.ConnectServiceConfiguration.Port)) + return net.Listen("tcp", address) } func (s *ConnectService) ResolveBoolean( From 65e04cc571244dba8307b69d4a38478079e7a780 Mon Sep 17 00:00:00 2001 From: James-Milligan Date: Thu, 22 Sep 2022 10:51:10 +0100 Subject: [PATCH 19/21] linting fixes and test fix Signed-off-by: James-Milligan --- pkg/service/connect_service.go | 58 +++++++++++++++-------------- pkg/service/connect_service_test.go | 1 + 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/pkg/service/connect_service.go b/pkg/service/connect_service.go index 17e20286f..bd6b7fc7a 100644 --- a/pkg/service/connect_service.go +++ b/pkg/service/connect_service.go @@ -23,8 +23,8 @@ import ( type ConnectService struct { Eval eval.IEvaluator ConnectServiceConfiguration *ConnectServiceConfiguration - handler http.Handler tls bool + server http.Server } type ConnectServiceConfiguration struct { @@ -37,60 +37,65 @@ type ConnectServiceConfiguration struct { func (s *ConnectService) Serve(ctx context.Context, eval eval.IEvaluator) error { s.Eval = eval - mux := http.NewServeMux() - lis, err := s.buildListener() + lis, err := s.setupServer() if err != nil { return err } - path, handler := schemaConnectV1.NewServiceHandler(s) - s.handler = handler - mux.Handle(path, s.handler) - s.buildHandler(mux) - srv := http.Server{ - ReadTimeout: 2 * time.Second, - WriteTimeout: 4 * time.Second, - ReadHeaderTimeout: time.Second, - Handler: handler, - } errChan := make(chan error, 1) go func() { if s.tls { - if err := srv.ServeTLS( + if err := s.server.ServeTLS( lis, s.ConnectServiceConfiguration.ServerCertPath, s.ConnectServiceConfiguration.ServerKeyPath, ); err != nil && !errors.Is(err, http.ErrServerClosed) { errChan <- err } + fmt.Println("shutdown") } else { - if err := srv.Serve( + if err := s.server.Serve( lis, ); err != nil && !errors.Is(err, http.ErrServerClosed) { errChan <- err } + fmt.Println("shutdown") } close(errChan) }() <-ctx.Done() - if err := lis.Close(); err != nil { + if err := s.server.Shutdown(ctx); err != nil { return err } return <-errChan } -func (s *ConnectService) buildHandler(mux *http.ServeMux) { +func (s *ConnectService) setupServer() (net.Listener, error) { + var lis net.Listener + var err error + mux := http.NewServeMux() + if s.ConnectServiceConfiguration.ServerSocketPath != "" { + lis, err = net.Listen("unix", s.ConnectServiceConfiguration.ServerSocketPath) + } else { + address := net.JoinHostPort("localhost", fmt.Sprintf("%d", s.ConnectServiceConfiguration.Port)) + lis, err = net.Listen("tcp", address) + } + if err != nil { + return nil, err + } + path, handler := schemaConnectV1.NewServiceHandler(s) + mux.Handle(path, handler) if s.ConnectServiceConfiguration.ServerCertPath != "" && s.ConnectServiceConfiguration.ServerKeyPath != "" { s.tls = true if s.ConnectServiceConfiguration.CORS { - s.handler = newCORS().Handler(mux) + handler = newCORS().Handler(mux) } else { - s.handler = mux + handler = mux } } else { if s.ConnectServiceConfiguration.CORS { - s.handler = h2c.NewHandler( + handler = h2c.NewHandler( newCORS().Handler(mux), &http2.Server{}, ) @@ -101,14 +106,13 @@ func (s *ConnectService) buildHandler(mux *http.ServeMux) { ) } } -} - -func (s *ConnectService) buildListener() (net.Listener, error) { - if s.ConnectServiceConfiguration.ServerSocketPath != "" { - return net.Listen("unix", s.ConnectServiceConfiguration.ServerSocketPath) + s.server = http.Server{ + ReadTimeout: 2 * time.Second, + WriteTimeout: 4 * time.Second, + ReadHeaderTimeout: time.Second, + Handler: handler, } - address := net.JoinHostPort("localhost", fmt.Sprintf("%d", s.ConnectServiceConfiguration.Port)) - return net.Listen("tcp", address) + return lis, nil } func (s *ConnectService) ResolveBoolean( diff --git a/pkg/service/connect_service_test.go b/pkg/service/connect_service_test.go index caa1fce6c..2e68a5e74 100644 --- a/pkg/service/connect_service_test.go +++ b/pkg/service/connect_service_test.go @@ -96,6 +96,7 @@ func TestConnectService_UnixConnection(t *testing.T) { fmt.Sprintf("unix://%s", tt.socketPath), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock(), + grpc.WithTimeout(2*time.Second), ) if err != nil { log.Errorf("grpc - fail to dial: %v", err) From 3cc0bcac8c91db51471186450b19ffdb8800fba8 Mon Sep 17 00:00:00 2001 From: James-Milligan Date: Thu, 22 Sep 2022 16:23:48 +0100 Subject: [PATCH 20/21] added error prefix for parsing, added cors flag Signed-off-by: James-Milligan --- cmd/start.go | 6 ++--- pkg/runtime/runtime.go | 2 +- pkg/service/connect_service.go | 43 ++++++++++++---------------------- 3 files changed, 19 insertions(+), 32 deletions(-) diff --git a/cmd/start.go b/cmd/start.go index a52fc3eb8..d3ba7869e 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -20,7 +20,7 @@ const ( serverKeyPathFlagName = "server-key-path" uriFlagName = "uri" bearerTokenFlagName = "bearer-token" - corsFlagName = "cors" + corsFlagName = "cors-origin" ) func init() { @@ -48,7 +48,7 @@ func init() { "flags with the same key, the later will be used.") flags.StringP( bearerTokenFlagName, "b", "", "Set a bearer token to use for remote sync") - flags.BoolP(corsFlagName, "C", false, "Set CORS enable all headers on responses") + flags.StringSliceP(corsFlagName, "C", []string{}, "CORS allowed origins, * will allow all origins") _ = viper.BindPFlag(portFlagName, flags.Lookup(portFlagName)) _ = viper.BindPFlag(socketPathFlagName, flags.Lookup(socketPathFlagName)) @@ -90,7 +90,7 @@ var startCmd = &cobra.Command{ Evaluator: viper.GetString(evaluatorFlagName), - CORS: viper.GetBool(corsFlagName), + CORS: viper.GetStringSlice(corsFlagName), }) if err != nil { log.Error(err) diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go index e123f6409..55ac5b607 100644 --- a/pkg/runtime/runtime.go +++ b/pkg/runtime/runtime.go @@ -33,7 +33,7 @@ type Config struct { SyncBearerToken string Evaluator string - CORS bool + CORS []string } func (r *Runtime) startSyncer(ctx context.Context, syncr sync.ISync) error { diff --git a/pkg/service/connect_service.go b/pkg/service/connect_service.go index bd6b7fc7a..5c7ee9f8e 100644 --- a/pkg/service/connect_service.go +++ b/pkg/service/connect_service.go @@ -20,6 +20,8 @@ import ( "google.golang.org/protobuf/types/known/structpb" ) +const ErrorPrefix = "FlagdError:" + type ConnectService struct { Eval eval.IEvaluator ConnectServiceConfiguration *ConnectServiceConfiguration @@ -32,7 +34,7 @@ type ConnectServiceConfiguration struct { ServerCertPath string ServerKeyPath string ServerSocketPath string - CORS bool + CORS []string } func (s *ConnectService) Serve(ctx context.Context, eval eval.IEvaluator) error { @@ -53,14 +55,12 @@ func (s *ConnectService) Serve(ctx context.Context, eval eval.IEvaluator) error ); err != nil && !errors.Is(err, http.ErrServerClosed) { errChan <- err } - fmt.Println("shutdown") } else { if err := s.server.Serve( lis, ); err != nil && !errors.Is(err, http.ErrServerClosed) { errChan <- err } - fmt.Println("shutdown") } close(errChan) }() @@ -88,23 +88,12 @@ func (s *ConnectService) setupServer() (net.Listener, error) { mux.Handle(path, handler) if s.ConnectServiceConfiguration.ServerCertPath != "" && s.ConnectServiceConfiguration.ServerKeyPath != "" { s.tls = true - if s.ConnectServiceConfiguration.CORS { - handler = newCORS().Handler(mux) - } else { - handler = mux - } + handler = s.newCORS().Handler(mux) } else { - if s.ConnectServiceConfiguration.CORS { - handler = h2c.NewHandler( - newCORS().Handler(mux), - &http2.Server{}, - ) - } else { - h2c.NewHandler( - newCORS().Handler(mux), - &http2.Server{}, - ) - } + handler = h2c.NewHandler( + s.newCORS().Handler(mux), + &http2.Server{}, + ) } s.server = http.Server{ ReadTimeout: 2 * time.Second, @@ -204,7 +193,7 @@ func (s *ConnectService) ResolveObject( return res, nil } -func newCORS() *cors.Cors { +func (s *ConnectService) newCORS() *cors.Cors { return cors.New(cors.Options{ AllowedMethods: []string{ http.MethodHead, @@ -214,10 +203,7 @@ func newCORS() *cors.Cors { http.MethodPatch, http.MethodDelete, }, - AllowOriginFunc: func(origin string) bool { - // Allow all origins, which effectively disables CORS. - return true - }, + AllowedOrigins: s.ConnectServiceConfiguration.CORS, AllowedHeaders: []string{"*"}, ExposedHeaders: []string{ // Content-Type is in the default safelist. @@ -239,13 +225,14 @@ func newCORS() *cors.Cors { func errFormat(err error) error { switch err.Error() { case model.FlagNotFoundErrorCode: - return connect.NewError(connect.CodeNotFound, err) + return connect.NewError(connect.CodeNotFound, fmt.Errorf("%s, %s", ErrorPrefix, err.Error())) case model.TypeMismatchErrorCode: - return connect.NewError(connect.CodeInvalidArgument, err) + return connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("%s, %s", ErrorPrefix, err.Error())) case model.DisabledReason: - return connect.NewError(connect.CodeUnavailable, err) + return connect.NewError(connect.CodeUnavailable, fmt.Errorf("%s, %s", ErrorPrefix, err.Error())) case model.ParseErrorCode: - return connect.NewError(connect.CodeDataLoss, err) + return connect.NewError(connect.CodeDataLoss, fmt.Errorf("%s, %s", ErrorPrefix, err.Error())) } + return err } From 3f1b61ee7f5ddef9d1487dbbbd26b70f76ca7928 Mon Sep 17 00:00:00 2001 From: James-Milligan Date: Mon, 26 Sep 2022 09:14:40 +0100 Subject: [PATCH 21/21] cors headers comment Signed-off-by: James-Milligan --- docs/configuration.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/configuration.md b/docs/configuration.md index 88d963c5d..365c0d78b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -16,6 +16,7 @@ Supported flags are as follows (result of running `./flagd start --help`): -d, --socket-path string Set the flagd socket path. -y, --sync-provider string Set a sync provider e.g. filepath or remote (default "filepath") -f, --uri strings Set a sync provider uri to read data from this can be a filepath or url. Using multiple providers is supported where collisions between flags with the same key, the later will be used. + -C, --cors-origin strings Set a CORS allow origin header, setting "*" will allow all origins (by default CORS headers are not set) ``` Environment variable keys are uppercased, prefixed with `FLAGD_` and all `-` are replaced with `_`. For example,