diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 581add0bd..5b2d4f2db 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -2,10 +2,10 @@ "hooks/open-telemetry": "0.3.4", "hooks/validator": "0.1.6", "providers/configcat": "0.2.1", - "providers/flagd": "0.2.5", + "providers/flagd": "0.2.6", "providers/flipt": "0.1.3", "providers/from-env": "0.1.5", - "providers/go-feature-flag": "0.2.3", + "providers/go-feature-flag": "0.2.4", "providers/gcp": "0.0.1", "providers/flagsmith": "0.1.4", "providers/launchdarkly": "0.1.5", @@ -15,5 +15,6 @@ "providers/ofrep": "0.1.5", "providers/prefab": "0.0.2", "tests/flagd": "1.4.1", - "providers/go-feature-flag-in-process": "0.1.0" + "providers/go-feature-flag-in-process": "0.1.0", + "providers/multi-provider": "0.0.2" } diff --git a/providers/flagd/CHANGELOG.md b/providers/flagd/CHANGELOG.md index 140e95228..2bfcb0fba 100644 --- a/providers/flagd/CHANGELOG.md +++ b/providers/flagd/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [0.2.6](https://github.com/open-feature/go-sdk-contrib/compare/providers/flagd/v0.2.5...providers/flagd/v0.2.6) (2025-02-22) + + +### 🐛 Bug Fixes + +* **flagd:** Fixed possible nil pointer exception with svcMetadata in service.go ([#634](https://github.com/open-feature/go-sdk-contrib/issues/634)) ([50256e9](https://github.com/open-feature/go-sdk-contrib/commit/50256e9af89201ca09f3989161afd5a069d3a06e)) + + +### ✨ New Features + +* **flagd:** Added WithGrpcDialOptionsOverride provider option ([#638](https://github.com/open-feature/go-sdk-contrib/issues/638)) ([fe904bb](https://github.com/open-feature/go-sdk-contrib/commit/fe904bb054be86ca8e1cafa8577e8ac152dfefc8)) + + +### 🧹 Chore + +* **flagd:** Updates flagd core to v0.11.2 ([#636](https://github.com/open-feature/go-sdk-contrib/issues/636)) ([99d1a0c](https://github.com/open-feature/go-sdk-contrib/commit/99d1a0c9d206102774c8a83b2f40e2a33b29309f)) + ## [0.2.5](https://github.com/open-feature/go-sdk-contrib/compare/providers/flagd/v0.2.4...providers/flagd/v0.2.5) (2025-02-18) diff --git a/providers/flagd/README.md b/providers/flagd/README.md index d9788ad0b..463d92c0c 100644 --- a/providers/flagd/README.md +++ b/providers/flagd/README.md @@ -144,6 +144,27 @@ openfeature.SetProvider(flagd.NewProvider( )) ``` +### gRPC DialOptions override + +The `GrpcDialOptionsOverride` is meant for connection of the in-process resolver to a Sync API implementation on a host/port, +that might require special credentials or headers. + +```go +creds := customSync.CreateCredentials(...) + +dialOptions := []grpc.DialOption{ + grpc.WithTransportCredentials(creds.TransportCredentials()), + grpc.WithPerRPCCredentials(creds.PerRPCCredentials()), + grpc.WithAuthority(...), + } + +openfeature.SetProvider(flagd.NewProvider( + flagd.WithInProcessResolver(), + flagd.WithHost("example.com/flagdSyncApi"), flagd.WithPort(443), + flagd.WithGrpcDialOptionsOverride(dialOptions), + )) +``` + ## Supported Events The flagd provider emits `PROVIDER_READY`, `PROVIDER_ERROR` and `PROVIDER_CONFIGURATION_CHANGED` events. diff --git a/providers/flagd/go.mod b/providers/flagd/go.mod index 30f9d62b2..c4b680ea2 100644 --- a/providers/flagd/go.mod +++ b/providers/flagd/go.mod @@ -7,47 +7,39 @@ toolchain go1.23.6 require ( buf.build/gen/go/open-feature/flagd/connectrpc/go v1.17.0-20240906125204-0a6a901b42e8.1 buf.build/gen/go/open-feature/flagd/grpc/go v1.5.1-20250127221518-be6d1143b690.2 - buf.build/gen/go/open-feature/flagd/protocolbuffers/go v1.36.4-20250127221518-be6d1143b690.1 + buf.build/gen/go/open-feature/flagd/protocolbuffers/go v1.36.5-20250127221518-be6d1143b690.1 connectrpc.com/connect v1.18.1 connectrpc.com/otelconnect v0.7.1 github.com/cucumber/godog v0.15.0 github.com/go-logr/logr v1.4.2 github.com/google/go-cmp v0.6.0 github.com/hashicorp/golang-lru/v2 v2.0.7 - github.com/open-feature/flagd/core v0.11.1 + github.com/open-feature/flagd/core v0.11.2 github.com/open-feature/go-sdk v1.11.0 github.com/open-feature/go-sdk-contrib/tests/flagd v1.4.1 go.uber.org/mock v0.5.0 - golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c + go.uber.org/zap v1.27.0 + golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac golang.org/x/net v0.34.0 google.golang.org/grpc v1.70.0 - google.golang.org/protobuf v1.36.4 - sigs.k8s.io/controller-runtime v0.19.0 + google.golang.org/protobuf v1.36.5 ) require ( github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect github.com/cucumber/gherkin/go/v26 v26.2.0 // indirect github.com/cucumber/messages/go/v21 v21.0.1 // indirect - github.com/diegoholiveira/jsonlogic/v3 v3.7.3 // indirect + github.com/diegoholiveira/jsonlogic/v3 v3.7.4 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-logr/zapr v1.3.0 // indirect github.com/gofrs/uuid v4.3.1+incompatible // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/gofuzz v1.2.0 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-memdb v1.3.4 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect - github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect github.com/open-feature/flagd-schemas v0.2.9-0.20250127221449-bb763438abc5 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/twmb/murmur3 v1.1.8 // indirect - github.com/x448/float16 v0.8.4 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect @@ -57,17 +49,9 @@ require ( go.opentelemetry.io/otel/metric v1.34.0 // indirect go.opentelemetry.io/otel/trace v1.34.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect - golang.org/x/mod v0.22.0 // indirect - golang.org/x/sys v0.29.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect - gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apimachinery v0.31.4 // indirect - k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect - sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect ) diff --git a/providers/flagd/go.sum b/providers/flagd/go.sum index c9a3a885a..edad9c778 100644 --- a/providers/flagd/go.sum +++ b/providers/flagd/go.sum @@ -2,8 +2,8 @@ buf.build/gen/go/open-feature/flagd/connectrpc/go v1.17.0-20240906125204-0a6a901 buf.build/gen/go/open-feature/flagd/connectrpc/go v1.17.0-20240906125204-0a6a901b42e8.1/go.mod h1:jKw7gioqYsWaHUKr5Ja6MiadsXcrGJxQ86gucJ0luUA= buf.build/gen/go/open-feature/flagd/grpc/go v1.5.1-20250127221518-be6d1143b690.2 h1:D3HI5RQbqgffyf+Z77+hReDx5kigFVAKGvttULD9/ms= buf.build/gen/go/open-feature/flagd/grpc/go v1.5.1-20250127221518-be6d1143b690.2/go.mod h1:b9rfG6rbGXZAlLwQwedvZ0kI0nUcR+aLaYF70pj920E= -buf.build/gen/go/open-feature/flagd/protocolbuffers/go v1.36.4-20250127221518-be6d1143b690.1 h1:0vXmOkGv8nO5H1W8TkSz+GtZhvD2LNXiQaiucEio6vk= -buf.build/gen/go/open-feature/flagd/protocolbuffers/go v1.36.4-20250127221518-be6d1143b690.1/go.mod h1:wemFLfCpuNfhrBQ7NwzbtYxbg+IihAYqJcNeS+fLpLI= +buf.build/gen/go/open-feature/flagd/protocolbuffers/go v1.36.5-20250127221518-be6d1143b690.1 h1:eZKupK8gUTuc6zifAFQon8Gnt44fR4cd0GnTWjELvEw= +buf.build/gen/go/open-feature/flagd/protocolbuffers/go v1.36.5-20250127221518-be6d1143b690.1/go.mod h1:wJvVIADHM0IaBc5sYf8wgMMgSHi0nAtc6rgr5rfizhA= connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= connectrpc.com/otelconnect v0.7.1 h1:scO5pOb0i4yUE66CnNrHeK1x51yq0bE0ehPg6WvzXJY= @@ -13,8 +13,7 @@ github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df/go.mod h1:h github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cucumber/gherkin/go/v26 v26.2.0 h1:EgIjePLWiPeslwIWmNQ3XHcypPsWAHoMCz/YEBKP4GI= github.com/cucumber/gherkin/go/v26 v26.2.0/go.mod h1:t2GAPnB8maCT4lkHL99BDCVNzCh1d7dBhCLt150Nr/0= -github.com/cucumber/godog v0.14.1 h1:HGZhcOyyfaKclHjJ+r/q93iaTJZLKYW6Tv3HkmUE6+M= -github.com/cucumber/godog v0.14.1/go.mod h1:FX3rzIDybWABU4kuIXLZ/qtqEe1Ac5RdXmqvACJOces= +github.com/cucumber/godog v0.15.0 h1:51AL8lBXF3f0cyA5CV4TnJFCTHpgiy+1x1Hb3TtZUmo= github.com/cucumber/godog v0.15.0/go.mod h1:FX3rzIDybWABU4kuIXLZ/qtqEe1Ac5RdXmqvACJOces= github.com/cucumber/messages/go/v21 v21.0.1 h1:wzA0LxwjlWQYZd32VTlAVDTkW6inOFmSM+RuOwHZiMI= github.com/cucumber/messages/go/v21 v21.0.1/go.mod h1:zheH/2HS9JLVFukdrsPWoPdmUtmYQAQPLk7w5vWsk5s= @@ -23,38 +22,24 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/diegoholiveira/jsonlogic/v3 v3.7.3 h1:orvGaTW5Qdcowpr7vgfMNuqYNjQYcqtw9W1Bk4QyI38= -github.com/diegoholiveira/jsonlogic/v3 v3.7.3/go.mod h1:OYRb6FSTVmMM+MNQ7ElmMsczyNSepw+OU4Z8emDSi4w= +github.com/diegoholiveira/jsonlogic/v3 v3.7.4 h1:92HSmB9bwM/o0ZvrCpcvTP2EsPXSkKtAniIr2W/dcIM= +github.com/diegoholiveira/jsonlogic/v3 v3.7.4/go.mod h1:OYRb6FSTVmMM+MNQ7ElmMsczyNSepw+OU4Z8emDSi4w= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= -github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI= github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM= -github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= @@ -71,10 +56,6 @@ github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uG github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -84,19 +65,10 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= -github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= -github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= -github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= github.com/open-feature/flagd-schemas v0.2.9-0.20250127221449-bb763438abc5 h1:0RKCLYeQpvSsKR95kc894tm8GAZmq7bcG48v0KJ0HCs= github.com/open-feature/flagd-schemas v0.2.9-0.20250127221449-bb763438abc5/go.mod h1:WKtwo1eW9/K6D+4HfgTXWBqCDzpvMhDa5eRxW7R5B2U= -github.com/open-feature/flagd/core v0.11.1 h1:0qBVXcRBZOFoZ5lNK/Yba2IyUDdxUHcLsv5OhUJtltA= -github.com/open-feature/flagd/core v0.11.1/go.mod h1:yzPjp7D9wNusvOyKt8wBND5PQGslcu+5e+xmaIBGgLE= +github.com/open-feature/flagd/core v0.11.2 h1:3LAuLR2vXpBF80RwwCAu9JX898JasfPH7ErJEf5C5YA= +github.com/open-feature/flagd/core v0.11.2/go.mod h1:rTdYFyLGP1tGwxo0X26ygIrC31nINCv8hcCq+vhFD0E= github.com/open-feature/go-sdk v1.11.0 h1:4cp9rXl16ZvlMCef7O+I3vQSXae8DzAF0SfV9mvYInw= github.com/open-feature/go-sdk v1.11.0/go.mod h1:+rkJhLBtYsJ5PZNddAgFILhRAAxwrJ32aU7UEUm4zQI= github.com/open-feature/go-sdk-contrib/tests/flagd v1.4.1 h1:kFAeX4qLSdcm//s9fvyBQ9CvfBZMtTkwEN46rjuxx+E= @@ -122,8 +94,6 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg= github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= -github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= -github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -131,8 +101,6 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= @@ -157,76 +125,28 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc= -golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= -golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= -golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.31.4 h1:I2QNzitPVsPeLQvexMEsj945QumYraqv9m74isPDKhM= -k8s.io/api v0.31.4/go.mod h1:d+7vgXLvmcdT1BCo79VEgJxHHryww3V5np2OYTr6jdw= -k8s.io/apimachinery v0.31.4 h1:8xjE2C4CzhYVm9DGf60yohpNUh5AEBnPxCryPBECmlM= -k8s.io/apimachinery v0.31.4/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= -k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= -k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= -k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q= -sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/providers/flagd/pkg/configuration.go b/providers/flagd/pkg/configuration.go index e08536096..61538fb8a 100644 --- a/providers/flagd/pkg/configuration.go +++ b/providers/flagd/pkg/configuration.go @@ -8,6 +8,7 @@ import ( "github.com/go-logr/logr" "github.com/open-feature/flagd/core/pkg/sync" "github.com/open-feature/go-sdk-contrib/providers/flagd/internal/cache" + "google.golang.org/grpc" ) type ResolverType string @@ -58,6 +59,7 @@ type providerConfiguration struct { TLSEnabled bool CustomSyncProvider sync.ISync CustomSyncProviderUri string + GrpcDialOptionsOverride []grpc.DialOption log logr.Logger } diff --git a/providers/flagd/pkg/provider.go b/providers/flagd/pkg/provider.go index c41165e22..d6c413db4 100644 --- a/providers/flagd/pkg/provider.go +++ b/providers/flagd/pkg/provider.go @@ -13,6 +13,7 @@ import ( process "github.com/open-feature/go-sdk-contrib/providers/flagd/pkg/service/in_process" rpcService "github.com/open-feature/go-sdk-contrib/providers/flagd/pkg/service/rpc" of "github.com/open-feature/go-sdk/openfeature" + "google.golang.org/grpc" ) const ( @@ -78,15 +79,16 @@ func NewProvider(opts ...ProviderOption) *Provider { provider.providerConfiguration.EventStreamConnectionMaxAttempts) } else { service = process.NewInProcessService(process.Configuration{ - Host: provider.providerConfiguration.Host, - Port: provider.providerConfiguration.Port, - ProviderID: provider.providerConfiguration.ProviderID, - Selector: provider.providerConfiguration.Selector, - TargetUri: provider.providerConfiguration.TargetUri, - TLSEnabled: provider.providerConfiguration.TLSEnabled, - OfflineFlagSource: provider.providerConfiguration.OfflineFlagSourcePath, - CustomSyncProvider: provider.providerConfiguration.CustomSyncProvider, - CustomSyncProviderUri: provider.providerConfiguration.CustomSyncProviderUri, + Host: provider.providerConfiguration.Host, + Port: provider.providerConfiguration.Port, + ProviderID: provider.providerConfiguration.ProviderID, + Selector: provider.providerConfiguration.Selector, + TargetUri: provider.providerConfiguration.TargetUri, + TLSEnabled: provider.providerConfiguration.TLSEnabled, + OfflineFlagSource: provider.providerConfiguration.OfflineFlagSourcePath, + CustomSyncProvider: provider.providerConfiguration.CustomSyncProvider, + CustomSyncProviderUri: provider.providerConfiguration.CustomSyncProviderUri, + GrpcDialOptionsOverride: provider.providerConfiguration.GrpcDialOptionsOverride, }) } @@ -356,3 +358,13 @@ func WithCustomSyncProviderAndUri(customSyncProvider sync.ISync, customSyncProvi p.providerConfiguration.CustomSyncProviderUri = customSyncProviderUri } } + +// WithGrpcDialOptionsOverride provides a set of custom grps.DialOption that will fully override the gRPC dial options used by +// the InProcess resolver with gRPC syncer. All the other provider options that also set dial options (e.g. WithTLS, or WithCertificatePath) +// will be silently ignored. +// This is only useful with inProcess resolver type +func WithGrpcDialOptionsOverride(grpcDialOptionsOverride []grpc.DialOption) ProviderOption { + return func(p *Provider) { + p.providerConfiguration.GrpcDialOptionsOverride = grpcDialOptionsOverride + } +} diff --git a/providers/flagd/pkg/provider_test.go b/providers/flagd/pkg/provider_test.go index 53a57dcfa..d7b03fbdc 100644 --- a/providers/flagd/pkg/provider_test.go +++ b/providers/flagd/pkg/provider_test.go @@ -1,6 +1,7 @@ package flagd import ( + "reflect" "testing" "github.com/open-feature/flagd/core/pkg/sync" @@ -9,29 +10,36 @@ import ( process "github.com/open-feature/go-sdk-contrib/providers/flagd/pkg/service/in_process" of "github.com/open-feature/go-sdk/openfeature" "go.uber.org/mock/gomock" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" ) func TestNewProvider(t *testing.T) { customSyncProvider := process.NewDoNothingCustomSyncProvider() + gRPCDialOptionOverride := []grpc.DialOption{ + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithAuthority("test-authority"), + } tests := []struct { - name string - expectedResolver ResolverType - expectPort uint16 - expectHost string - expectTargetUri string - expectCacheType cache.Type - expectCertPath string - expectMaxRetries int - expectCacheSize int - expectOtelIntercept bool - expectSocketPath string - expectTlsEnabled bool - expectProviderID string - expectSelector string - expectCustomSyncProvider sync.ISync - expectCustomSyncProviderUri string - options []ProviderOption + name string + expectedResolver ResolverType + expectPort uint16 + expectHost string + expectTargetUri string + expectCacheType cache.Type + expectCertPath string + expectMaxRetries int + expectCacheSize int + expectOtelIntercept bool + expectSocketPath string + expectTlsEnabled bool + expectProviderID string + expectSelector string + expectCustomSyncProvider sync.ISync + expectCustomSyncProviderUri string + expectGrpcDialOptionsOverride []grpc.DialOption + options []ProviderOption }{ { name: "default construction", @@ -173,6 +181,20 @@ func TestNewProvider(t *testing.T) { WithCustomSyncProvider(customSyncProvider), }, }, + { + name: "with gRPC DialOptions override with in-process resolver", + expectedResolver: inProcess, + expectHost: defaultHost, + expectPort: defaultInProcessPort, + expectCacheType: defaultCache, + expectCacheSize: defaultMaxCacheSize, + expectMaxRetries: defaultMaxEventStreamRetries, + expectGrpcDialOptionsOverride: gRPCDialOptionOverride, + options: []ProviderOption{ + WithInProcessResolver(), + WithGrpcDialOptionsOverride(gRPCDialOptionOverride), + }, + }, { name: "with selector and providerID with in-process resolver", expectedResolver: inProcess, @@ -295,6 +317,18 @@ func TestNewProvider(t *testing.T) { test.expectCustomSyncProviderUri, config.CustomSyncProviderUri) } + if test.expectGrpcDialOptionsOverride != nil { + if config.GrpcDialOptionsOverride == nil { + t.Errorf("incorrent configuration GrpcDialOptionsOverride, expected %v, got nil", config.GrpcDialOptionsOverride) + } else if !reflect.DeepEqual(config.GrpcDialOptionsOverride, test.expectGrpcDialOptionsOverride) { + t.Errorf("incorrent configuration GrpcDialOptionsOverride, expected %v, got %v", test.expectGrpcDialOptionsOverride, config.GrpcDialOptionsOverride) + } + } else { + if config.GrpcDialOptionsOverride != nil { + t.Errorf("incorrent configuration GrpcDialOptionsOverride, expected nil, got %v", config.GrpcDialOptionsOverride) + } + } + // this line will fail linting if this provider is no longer compatible with the openfeature sdk var _ of.FeatureProvider = flagdProvider }) diff --git a/providers/flagd/pkg/service/in_process/service.go b/providers/flagd/pkg/service/in_process/service.go index 4031b5fe7..8dd75688e 100644 --- a/providers/flagd/pkg/service/in_process/service.go +++ b/providers/flagd/pkg/service/in_process/service.go @@ -7,6 +7,8 @@ import ( "regexp" parallel "sync" + googlegrpc "google.golang.org/grpc" + "github.com/open-feature/flagd/core/pkg/evaluator" "github.com/open-feature/flagd/core/pkg/logger" "github.com/open-feature/flagd/core/pkg/model" @@ -17,7 +19,6 @@ import ( "github.com/open-feature/flagd/core/pkg/sync/grpc/credentials" of "github.com/open-feature/go-sdk/openfeature" "golang.org/x/exp/maps" - "sigs.k8s.io/controller-runtime/pkg/log/zap" ) // InProcess service implements flagd flag evaluation in-process. @@ -33,19 +34,20 @@ type InProcess struct { } type Configuration struct { - Host any - Port any - TargetUri string - ProviderID string - Selector string - TLSEnabled bool - OfflineFlagSource string - CustomSyncProvider sync.ISync - CustomSyncProviderUri string + Host any + Port any + TargetUri string + ProviderID string + Selector string + TLSEnabled bool + OfflineFlagSource string + CustomSyncProvider sync.ISync + CustomSyncProviderUri string + GrpcDialOptionsOverride []googlegrpc.DialOption } func NewInProcessService(cfg Configuration) *InProcess { - log := logger.NewLogger(zap.NewRaw(), false) + log := logger.NewLogger(NewRaw(), false) iSync, uri := makeSyncProvider(cfg, log) @@ -301,12 +303,13 @@ func makeSyncProvider(cfg Configuration, log *logger.Logger) (sync.ISync, string log.Info("operating in in-process mode with flags sourced from " + uri) return &grpc.Sync{ - CredentialBuilder: &credentials.CredentialBuilder{}, - Logger: log, - Secure: cfg.TLSEnabled, - ProviderID: cfg.ProviderID, - Selector: cfg.Selector, - URI: uri, + CredentialBuilder: &credentials.CredentialBuilder{}, + GrpcDialOptionsOverride: cfg.GrpcDialOptionsOverride, + Logger: log, + Secure: cfg.TLSEnabled, + ProviderID: cfg.ProviderID, + Selector: cfg.Selector, + URI: uri, }, uri } diff --git a/providers/flagd/pkg/service/in_process/zap.go b/providers/flagd/pkg/service/in_process/zap.go new file mode 100644 index 000000000..2a888f885 --- /dev/null +++ b/providers/flagd/pkg/service/in_process/zap.go @@ -0,0 +1,47 @@ +package process + +import ( + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "os" + "time" +) + +// EncoderConfigOption is a function that can modify a `zapcore.EncoderConfig`. +type EncoderConfigOption func(*zapcore.EncoderConfig) + +func newJSONEncoder(opts ...EncoderConfigOption) zapcore.Encoder { + encoderConfig := zap.NewProductionEncoderConfig() + for _, opt := range opts { + opt(&encoderConfig) + } + return zapcore.NewJSONEncoder(encoderConfig) +} + +// NewRaw returns a new zap.Logger configured with the passed Opts +// or their defaults. It uses KubeAwareEncoder which adds Type +// information and Namespace/Name to the log. +func NewRaw() *zap.Logger { + level := zap.NewAtomicLevelAt(zap.InfoLevel) + + var zapOpts []zap.Option + if level.Enabled(zapcore.Level(-2)) { + zapOpts = append(zapOpts, + zap.WrapCore(func(core zapcore.Core) zapcore.Core { + return zapcore.NewSamplerWithOptions(core, time.Second, 100, 100) + })) + } + zapOpts = append(zapOpts, zap.AddStacktrace(zap.NewAtomicLevelAt(zap.ErrorLevel))) + + f := func(ecfg *zapcore.EncoderConfig) { + ecfg.EncodeTime = zapcore.RFC3339TimeEncoder + } + encoder := newJSONEncoder(f) + + // this basically mimics NewConfig, but with a custom sink + sink := zapcore.AddSync(os.Stderr) + zapOpts = append(zapOpts, zap.ErrorOutput(sink)) + log := zap.New(zapcore.NewCore(encoder, sink, level)) + log = log.WithOptions(zapOpts...) + return log +} diff --git a/providers/go-feature-flag/CHANGELOG.md b/providers/go-feature-flag/CHANGELOG.md index 4562b944b..9d8e1abb8 100644 --- a/providers/go-feature-flag/CHANGELOG.md +++ b/providers/go-feature-flag/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.2.4](https://github.com/open-feature/go-sdk-contrib/compare/providers/go-feature-flag/v0.2.3...providers/go-feature-flag/v0.2.4) (2025-03-05) + + +### 🧹 Chore + +* **go-feature-flag:** Support new hook format ([#641](https://github.com/open-feature/go-sdk-contrib/issues/641)) ([192d45a](https://github.com/open-feature/go-sdk-contrib/commit/192d45ae37a0e10b6831a87ca02729d84171b911)) + ## [0.2.3](https://github.com/open-feature/go-sdk-contrib/compare/providers/go-feature-flag/v0.2.2...providers/go-feature-flag/v0.2.3) (2025-02-10) diff --git a/providers/go-feature-flag/pkg/hook/data_collector_hook.go b/providers/go-feature-flag/pkg/hook/data_collector_hook.go index 16e99de95..dd7a476a0 100644 --- a/providers/go-feature-flag/pkg/hook/data_collector_hook.go +++ b/providers/go-feature-flag/pkg/hook/data_collector_hook.go @@ -13,10 +13,11 @@ func NewDataCollectorHook(dataCollectorManager *controller.DataCollectorManager) } type dataCollectorHook struct { + openfeature.UnimplementedHook dataCollectorManager *controller.DataCollectorManager } -func (d *dataCollectorHook) After(ctx context.Context, hookCtx openfeature.HookContext, +func (d *dataCollectorHook) After(_ context.Context, hookCtx openfeature.HookContext, evalDetails openfeature.InterfaceEvaluationDetails, hint openfeature.HookHints) error { if evalDetails.Reason != openfeature.CachedReason { // we send it only when cached because the evaluation will be collected directly in the relay-proxy @@ -37,7 +38,7 @@ func (d *dataCollectorHook) After(ctx context.Context, hookCtx openfeature.HookC return nil } -func (d *dataCollectorHook) Error(ctx context.Context, hookCtx openfeature.HookContext, +func (d *dataCollectorHook) Error(_ context.Context, hookCtx openfeature.HookContext, err error, hint openfeature.HookHints) { event := model.FeatureEvent{ Kind: "feature", @@ -52,12 +53,3 @@ func (d *dataCollectorHook) Error(ctx context.Context, hookCtx openfeature.HookC } _ = d.dataCollectorManager.AddEvent(event) } - -func (d *dataCollectorHook) Before(context.Context, openfeature.HookContext, openfeature.HookHints) (*openfeature.EvaluationContext, error) { - // Do nothing, needed to satisfy the interface - return nil, nil -} - -func (d *dataCollectorHook) Finally(context.Context, openfeature.HookContext, openfeature.HookHints) { - // Do nothing, needed to satisfy the interface -} diff --git a/providers/go-feature-flag/pkg/hook/evaluation_enrichment_hook.go b/providers/go-feature-flag/pkg/hook/evaluation_enrichment_hook.go index 71462a8f0..dfdf74c79 100644 --- a/providers/go-feature-flag/pkg/hook/evaluation_enrichment_hook.go +++ b/providers/go-feature-flag/pkg/hook/evaluation_enrichment_hook.go @@ -10,20 +10,10 @@ func NewEvaluationEnrichmentHook(exporterMetadata map[string]interface{}) openfe } type evaluationEnrichmentHook struct { + openfeature.UnimplementedHook exporterMetadata map[string]interface{} } -func (d *evaluationEnrichmentHook) After(_ context.Context, _ openfeature.HookContext, - _ openfeature.InterfaceEvaluationDetails, _ openfeature.HookHints) error { - // Do nothing, needed to satisfy the interface - return nil -} - -func (d *evaluationEnrichmentHook) Error(_ context.Context, _ openfeature.HookContext, - _ error, _ openfeature.HookHints) { - // Do nothing, needed to satisfy the interface -} - func (d *evaluationEnrichmentHook) Before(_ context.Context, hookCtx openfeature.HookContext, _ openfeature.HookHints) (*openfeature.EvaluationContext, error) { attributes := hookCtx.EvaluationContext().Attributes() if goffSpecific, ok := attributes["gofeatureflag"]; ok { @@ -39,7 +29,3 @@ func (d *evaluationEnrichmentHook) Before(_ context.Context, hookCtx openfeature newCtx := openfeature.NewEvaluationContext(hookCtx.EvaluationContext().TargetingKey(), attributes) return &newCtx, nil } - -func (d *evaluationEnrichmentHook) Finally(context.Context, openfeature.HookContext, openfeature.HookHints) { - // Do nothing, needed to satisfy the interface -} diff --git a/providers/multi-provider/README.md b/providers/multi-provider/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/providers/multi-provider/go.mod b/providers/multi-provider/go.mod new file mode 100644 index 000000000..91a059021 --- /dev/null +++ b/providers/multi-provider/go.mod @@ -0,0 +1,10 @@ +module github.com/open-feature/go-sdk-contrib/providers/multi-provider + +go 1.23.0 + +require github.com/open-feature/go-sdk v1.14.1 + +require ( + github.com/go-logr/logr v1.4.2 // indirect + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect +) diff --git a/providers/multi-provider/go.sum b/providers/multi-provider/go.sum new file mode 100644 index 000000000..ac0b45fa2 --- /dev/null +++ b/providers/multi-provider/go.sum @@ -0,0 +1,10 @@ +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/open-feature/go-sdk v1.14.1 h1:jcxjCIG5Up3XkgYwWN5Y/WWfc6XobOhqrIwjyDBsoQo= +github.com/open-feature/go-sdk v1.14.1/go.mod h1:t337k0VB/t/YxJ9S0prT30ISUHwYmUd/jhUZgFcOvGg= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= diff --git a/providers/multi-provider/internal/aggregate-errors.go b/providers/multi-provider/internal/aggregate-errors.go new file mode 100644 index 000000000..5b7ccbcf0 --- /dev/null +++ b/providers/multi-provider/internal/aggregate-errors.go @@ -0,0 +1,40 @@ +package internal + +import ( + "encoding/json" + "fmt" +) + +// StateErr is how the error in the Init stage of a provider is reported. +type StateErr struct { + ProviderName string `json:"source"` + Err error `json:"-"` + ErrMessage string `json:"error"` +} + +func (e *StateErr) Error() string { + return fmt.Sprintf("Provider %s had an error: %v", e.ProviderName, e.Err) +} + +type AggregateError struct { + Message string `json:"message"` + Errors []StateErr `json:"errors"` +} + +func (ae *AggregateError) Error() string { + errorsJSON, err := json.Marshal(ae.Errors) + if err != nil { + return fmt.Sprintf("Error in json marshal of errors, %s", err) + } + + return fmt.Sprintf("%s\n%s", ae.Message, string(errorsJSON)) + +} + +func (ae *AggregateError) Construct(providerErrors []StateErr) { + // Show first error message for convenience, but all errors in the object + msg := fmt.Sprintf("Provider errors occurred: %s: %v", providerErrors[0].ProviderName, providerErrors[0].Err) + + ae.Message = msg + ae.Errors = providerErrors +} diff --git a/providers/multi-provider/pkg/providers.go b/providers/multi-provider/pkg/providers.go new file mode 100644 index 000000000..5b8b3363f --- /dev/null +++ b/providers/multi-provider/pkg/providers.go @@ -0,0 +1,216 @@ +package multiprovider + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "sync" + + err "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal" + + "github.com/open-feature/go-sdk/openfeature" + "github.com/open-feature/go-sdk/openfeature/hooks" +) + +var ( + errUniqueName = errors.New("provider names must be unique") +) + +// UniqueNameProvider allows for a unique name to be assigned to a provider during a multi-provider set up. +// The name will be used when reporting errors & results to specify the provider associated. +type UniqueNameProvider struct { + Provider openfeature.FeatureProvider + UniqueName string +} + +// MultiMetadata defines the return of the MultiProvider metadata with the aggregated data of all the providers. +type MultiMetadata struct { + Name string `json:"name"` + OriginalMetadata map[string]openfeature.Metadata `json:"originalMetadata"` +} + +// MultiProvider implements openfeature `FeatureProvider` in a way to accept an array of providers. +type MultiProvider struct { + providersEntries []UniqueNameProvider + providersEntriesByName map[string]UniqueNameProvider + AggregatedMetadata MultiMetadata + EvaluationStrategy string + events chan openfeature.Event + status openfeature.State + mu sync.Mutex +} + +// NewMultiProvider returns the unified interface of multiple providers for interaction. +func NewMultiProvider(passedProviders []UniqueNameProvider, evaluationStrategy string, logger *hooks.LoggingHook) (*MultiProvider, error) { + multiProvider := &MultiProvider{ + providersEntries: []UniqueNameProvider{}, + providersEntriesByName: map[string]UniqueNameProvider{}, + AggregatedMetadata: MultiMetadata{ + Name: "multiprovider", + OriginalMetadata: map[string]openfeature.Metadata{}, + }, + } + + err := registerProviders(multiProvider, passedProviders) + if err != nil { + return nil, err + } + + return multiProvider, nil +} + +func (mp *MultiProvider) Providers() []UniqueNameProvider { + return mp.providersEntries +} + +func (mp *MultiProvider) ProvidersByName() map[string]UniqueNameProvider { + return mp.providersEntriesByName +} + +func (mp *MultiProvider) ProviderByName(name string) (UniqueNameProvider, bool) { + provider, exists := mp.providersEntriesByName[name] + return provider, exists +} + +// registerProviders ensures that when setting up an instant of MultiProvider the providers provided either have a unique name or the base `metadata.Name` is made unique by adding an indexed based number to it. +// registerProviders also stores the providers by their unique name and in an array for easy usage. +func registerProviders(mp *MultiProvider, providers []UniqueNameProvider) error { + providersByName := make(map[string][]UniqueNameProvider) + + for _, provider := range providers { + uniqueName := provider.UniqueName + + if _, exists := providersByName[uniqueName]; exists { + return errUniqueName + } + + if uniqueName == "" { + providersByName[provider.Provider.Metadata().Name] = append(providersByName[provider.Provider.Metadata().Name], provider) + } else { + providersByName[uniqueName] = append(providersByName[uniqueName], provider) + } + } + + for name, providers := range providersByName { + if len(providers) == 1 { + providers[0].UniqueName = name + mp.providersEntries = append(mp.providersEntries, providers[0]) + mp.providersEntriesByName[name] = providers[0] + mp.AggregatedMetadata.OriginalMetadata[name] = providers[0].Provider.Metadata() + } else { + for i, provider := range providers { + uniqueName := fmt.Sprintf("%s-%d", name, i+1) + provider.UniqueName = uniqueName + mp.providersEntries = append(mp.providersEntries, provider) + mp.providersEntriesByName[uniqueName] = provider + mp.AggregatedMetadata.OriginalMetadata[uniqueName] = provider.Provider.Metadata() + } + } + } + return nil +} + +// Metadata provides the name `multiprovider` and the names of each provider passed. +func (mp *MultiProvider) Metadata() openfeature.Metadata { + metaJSON, _ := json.Marshal(mp.AggregatedMetadata) + + return openfeature.Metadata{Name: string(metaJSON)} +} + +// Hooks returns a collection of openfeature.Hook defined by this provider +func (mp *MultiProvider) Hooks() []openfeature.Hook { + // Hooks that should be included with the provider + return []openfeature.Hook{} +} + +// BooleanEvaluation returns a boolean flag +func (mp *MultiProvider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx openfeature.FlattenedContext) openfeature.BoolResolutionDetail { + // code to evaluate boolean + return openfeature.BoolResolutionDetail{} +} + +// StringEvaluation returns a string flag +func (mp *MultiProvider) StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx openfeature.FlattenedContext) openfeature.StringResolutionDetail { + // code to evaluate string + return openfeature.StringResolutionDetail{} +} + +// FloatEvaluation returns a float flag +func (mp *MultiProvider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx openfeature.FlattenedContext) openfeature.FloatResolutionDetail { + // code to evaluate float + return openfeature.FloatResolutionDetail{} +} + +// IntEvaluation returns an int flag +func (mp *MultiProvider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail { + // code to evaluate int + return openfeature.IntResolutionDetail{} +} + +// ObjectEvaluation returns an object flag +func (mp *MultiProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail { + // code to evaluate object + return openfeature.InterfaceResolutionDetail{} +} + +// Init will run the initialize method for all of provides and aggregate the errors. +func (mp *MultiProvider) Init(evalCtx openfeature.EvaluationContext) error { + var wg sync.WaitGroup + errChan := make(chan err.StateErr, len(mp.providersEntries)) + + for _, provider := range mp.providersEntries { + wg.Add(1) + go func(p UniqueNameProvider) { + defer wg.Done() + if stateHandle, ok := p.Provider.(openfeature.StateHandler); ok { + if initErr := stateHandle.Init(evalCtx); initErr != nil { + errChan <- err.StateErr{ProviderName: p.UniqueName, Err: initErr, ErrMessage: initErr.Error()} + } + } + }(provider) + } + + go func() { + wg.Wait() + close(errChan) + }() + + var errors []err.StateErr + for err := range errChan { + errors = append(errors, err) + } + + if len(errors) > 0 { + var aggErr err.AggregateError + aggErr.Construct(errors) + return &aggErr + } + + return nil +} + +func (mp *MultiProvider) Status() openfeature.State { + return openfeature.ReadyState +} + +func (mp *MultiProvider) Shutdown() { + var wg sync.WaitGroup + + for _, provider := range mp.providersEntries { + wg.Add(1) + go func(p UniqueNameProvider) { + defer wg.Done() + if stateHandle, ok := p.Provider.(openfeature.StateHandler); ok { + stateHandle.Shutdown() + } + }(provider) + } + + wg.Wait() +} + +func (mp *MultiProvider) EventChannel() <-chan openfeature.Event { + ev := make(chan openfeature.Event) + return ev +} diff --git a/providers/multi-provider/pkg/providers_test.go b/providers/multi-provider/pkg/providers_test.go new file mode 100644 index 000000000..16d7117e7 --- /dev/null +++ b/providers/multi-provider/pkg/providers_test.go @@ -0,0 +1,540 @@ +package multiprovider + +import ( + "encoding/json" + "fmt" + "strings" + "testing" + "time" + + errs "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal" + + "github.com/open-feature/go-sdk/openfeature" + "github.com/open-feature/go-sdk/openfeature/hooks" + oft "github.com/open-feature/go-sdk/openfeature/testing" +) + +// MockProvider utilizes openfeature's TestProvider to add testable Init & Shutdown methods to test the MultiProvider functionality +type MockProvider struct { + oft.TestProvider + InitCount *int + ShutCount *int + TestErr string + InitDelay int + ShutDelay int + MockMeta string +} + +func (m *MockProvider) Init(evalCtx openfeature.EvaluationContext) error { + if m.TestErr != "" { + return fmt.Errorf(m.TestErr) + } + + if m.InitDelay != 0 { + time.Sleep(time.Duration(m.InitDelay) * time.Millisecond) + } + *m.InitCount += 1 + return nil +} + +func (m *MockProvider) Shutdown() { + if m.ShutDelay != 0 { + time.Sleep(time.Duration(m.ShutDelay) * time.Millisecond) + } + *m.ShutCount += 1 +} + +func (m *MockProvider) Metadata() openfeature.Metadata { + return openfeature.Metadata{Name: m.MockMeta} +} + +func NewMockProvider(initCount *int, shutCount *int, testErr string, initDelay int, shutDelay int, meta string) *MockProvider { + return &MockProvider{ + TestProvider: oft.NewTestProvider(), + InitCount: initCount, + ShutCount: shutCount, + TestErr: testErr, + InitDelay: initDelay, + ShutDelay: shutDelay, + MockMeta: meta, + } +} + +func TestMultiProvider_ProvidersMethod(t *testing.T) { + testProvider1 := oft.NewTestProvider() + testProvider2 := oft.NewTestProvider() + + defaultLogger, err := hooks.NewLoggingHook(false) + if err != nil { + t.Errorf("Issue setting up logger,'%s'", err) + } + + mp, err := NewMultiProvider([]UniqueNameProvider{ + { + Provider: testProvider1, + UniqueName: "provider1", + }, { + Provider: testProvider2, + UniqueName: "provider2", + }, + }, "test", defaultLogger) + + if err != nil { + t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) + } + + providers := mp.Providers() + + if len(providers) != 2 { + t.Errorf("Expected there to be '2' providers as passed but got: '%d'", len(providers)) + } + + if providers[0].UniqueName != "provider1" { + t.Errorf("Expected unique provider name to be: 'provider1', got: '%s'", providers[0].UniqueName) + } + if providers[1].UniqueName != "provider2" { + t.Errorf("Expected unique provider name to be: 'provider2', got: '%s'", providers[1].UniqueName) + } +} + +func TestMultiProvider_ProvidersByNamesMethod(t *testing.T) { + testProvider1 := oft.NewTestProvider() + testProvider2 := oft.NewTestProvider() + + defaultLogger, err := hooks.NewLoggingHook(false) + if err != nil { + t.Errorf("Issue setting up logger,'%s'", err) + } + + mp, err := NewMultiProvider([]UniqueNameProvider{ + { + Provider: testProvider1, + UniqueName: "provider1", + }, { + Provider: testProvider2, + UniqueName: "provider2", + }, + }, "test", defaultLogger) + + if err != nil { + t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) + } + + providers := mp.ProvidersByName() + + if len(providers) != 2 { + t.Errorf("Expected there to be '2' providers as passed but got: '%d'", len(providers)) + } + + if provider, exists := providers["provider1"]; exists { + if provider.UniqueName != "provider1" { + t.Errorf("Expected unique provider name to be: 'provider1', got: '%s'", provider.UniqueName) + } + if provider.Provider != testProvider1 { + t.Errorf("Expected unique provider name to be: 'provider1', got: '%s'", provider.UniqueName) + } + } else { + t.Errorf("Expected there to be a provider with the key of '%s', but none was found.", "provider1") + } + + if provider, exists := providers["provider2"]; exists { + if provider.UniqueName != "provider2" { + t.Errorf("Expected unique provider name to be: 'provider2', got: '%s'", provider.UniqueName) + } + if provider.Provider != testProvider2 { + t.Errorf("Expected unique provider name to be: 'provider2', got: '%s'", provider.UniqueName) + } + } else { + t.Errorf("Expected there to be a provider with the key of '%s', but none was found.", "provider2") + } + +} + +func TestMultiProvider_ProviderByNameMethod(t *testing.T) { + testProvider1 := oft.NewTestProvider() + testProvider2 := oft.NewTestProvider() + + defaultLogger, err := hooks.NewLoggingHook(false) + if err != nil { + t.Errorf("Issue setting up logger,'%s'", err) + } + + mp, err := NewMultiProvider([]UniqueNameProvider{ + { + Provider: testProvider1, + UniqueName: "provider1", + }, { + Provider: testProvider2, + UniqueName: "provider2", + }, + }, "test", defaultLogger) + + if err != nil { + t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) + } + + providers := mp.ProvidersByName() + + if len(providers) != 2 { + t.Errorf("Expected there to be '2' providers as passed but got: '%d'", len(providers)) + } + if provider, exists := mp.ProviderByName("provider2"); exists { + if provider.UniqueName != "provider2" { + t.Errorf("Expected unique provider name to be: 'provider2', got: '%s'", provider.UniqueName) + } + if provider.Provider != testProvider2 { + t.Errorf("Expected unique provider name to be: 'provider2', got: '%s'", provider.UniqueName) + } + } else { + t.Errorf("Expected there to be a provider with the key of '%s', but none was found.", "provider1") + } + +} + +// todo: currently the `multiProvider.Metadata()` just give the `Name` of the multi provider it doesn't aggregate the passed providers as stated in this specification https://openfeature.dev/specification/appendix-a/#metadata so this test fails +func TestMultiProvider_MetaData(t *testing.T) { + initializations := 0 + shutdowns := 0 + + testProvider1 := oft.NewTestProvider() + testProvider2 := NewMockProvider(&initializations, &shutdowns, "", 0, 0, "test2") + + defaultLogger, err := hooks.NewLoggingHook(false) + if err != nil { + t.Errorf("Issue setting up logger,'%s'", err) + } + + mp, err := NewMultiProvider([]UniqueNameProvider{ + { + Provider: testProvider1, + UniqueName: "provider1", + }, { + Provider: testProvider2, + UniqueName: "provider2", + }, + }, "test", defaultLogger) + + if err != nil { + t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) + } + + expectedJSON, err := json.Marshal(MultiMetadata{ + Name: "multiprovider", + OriginalMetadata: map[string]openfeature.Metadata{ + "provider1": openfeature.Metadata{Name: "NoopProvider"}, + "provider2": openfeature.Metadata{Name: "test2"}, + }, + }) + if err != nil { + t.Errorf("Error in JSON marshal of the expected answer, '%s'", err) + } + + expectedMetadata := openfeature.Metadata{Name: string(expectedJSON)} + + if mp.Metadata() != expectedMetadata { + t.Errorf("Expected to see the aggregated metadata of all passed providers: '%s', got: '%s'", expectedMetadata, mp.Metadata()) + } +} + +func TestMultiProvider_Init(t *testing.T) { + initializations := 0 + shutdowns := 0 + + testProvider1 := NewMockProvider(&initializations, &shutdowns, "", 0, 0, "test1") + testProvider2 := oft.NewTestProvider() + testProvider3 := NewMockProvider(&initializations, &shutdowns, "", 1, 0, "test3") + + defaultLogger, err := hooks.NewLoggingHook(false) + if err != nil { + t.Errorf("Issue setting up logger,'%s'", err) + } + + mp, err := NewMultiProvider([]UniqueNameProvider{ + { + Provider: testProvider1, + UniqueName: "provider1", + }, { + Provider: testProvider2, + UniqueName: "provider2", + }, { + Provider: testProvider3, + UniqueName: "provider3", + }, + }, "test", defaultLogger) + + if err != nil { + t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) + } + + attributes := map[string]interface{}{ + "foo": "bar", + } + evalCtx := openfeature.NewTargetlessEvaluationContext(attributes) + + err = mp.Init(evalCtx) + if err != nil { + t.Errorf("Expected the initialization process to be successful, got error: '%s'", err) + } + + if initializations == 0 { + t.Errorf("Expected there to be initializations, but none were ran.") + } + + if initializations != 2 { + t.Errorf("Expected there to be '2' init steps ran, but got: '%d'.", initializations) + } + +} + +func TestMultiProvider_InitErrorWithProvider(t *testing.T) { + initializations := 0 + shutdowns := 0 + + testProvider1 := oft.NewTestProvider() + testProvider2 := NewMockProvider(&initializations, &shutdowns, "test error 1 end", 0, 0, "test2") + testProvider3 := NewMockProvider(&initializations, &shutdowns, "test error 2 end", 0, 0, "test3") + + defaultLogger, err := hooks.NewLoggingHook(false) + if err != nil { + t.Errorf("Issue setting up logger,'%s'", err) + } + + mp, err := NewMultiProvider([]UniqueNameProvider{ + { + Provider: testProvider1, + UniqueName: "provider1", + }, { + Provider: testProvider2, + UniqueName: "provider2", + }, { + Provider: testProvider3, + UniqueName: "provider3", + }, + }, "test", defaultLogger) + + if err != nil { + t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) + } + + attributes := map[string]interface{}{ + "foo": "bar", + } + evalCtx := openfeature.NewTargetlessEvaluationContext(attributes) + + err = mp.Init(evalCtx) + if err == nil { + t.Errorf("Expected the initialization process to throw an error.") + } + + var errors []errs.StateErr + + fullErr := err.Error() + fullErrArr := strings.SplitAfterN(fullErr, "end", 2) + errJSON := fullErrArr[1] + errMsg := fullErrArr[0] + + if !strings.Contains(errMsg, "Provider errors occurred:") { + t.Errorf("Expected the first line of error message to contain: '%s', got: '%s'", "Provider errors occurred:", errMsg) + } + + if err = json.Unmarshal([]byte(errJSON), &errors); err != nil { + t.Errorf("Failed to unmarshal error details: %v", err) + } + + if len(errors) != 2 { + t.Errorf("Expected there to be '2' errors found, got: '%d'", len(errors)) + } + +} + +func TestMultiProvider_Shutdown(t *testing.T) { + initializations := 0 + shutdowns := 0 + + testProvider1 := NewMockProvider(&initializations, &shutdowns, "", 0, 0, "test1") + testProvider2 := oft.NewTestProvider() + testProvider3 := NewMockProvider(&initializations, &shutdowns, "", 0, 2, "test3") + + defaultLogger, err := hooks.NewLoggingHook(false) + if err != nil { + t.Errorf("Issue setting up logger,'%s'", err) + } + + mp, err := NewMultiProvider([]UniqueNameProvider{ + { + Provider: testProvider1, + UniqueName: "provider1", + }, { + Provider: testProvider2, + UniqueName: "provider2", + }, { + Provider: testProvider3, + UniqueName: "provider3", + }, + }, "test", defaultLogger) + + if err != nil { + t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) + } + + mp.Shutdown() + + if shutdowns == 0 { + t.Errorf("Expected there to be shutdowns, but none were ran.") + } + + if shutdowns != 2 { + t.Errorf("Expected there to be '2' shutdown steps ran, but got: '%d'.", shutdowns) + } +} + +func TestNewMultiProvider_ProviderUniqueNames(t *testing.T) { + initializations := 0 + shutdowns := 0 + + testProvider1 := oft.NewTestProvider() + testProvider2 := NewMockProvider(&initializations, &shutdowns, "", 0, 0, "test2") + + defaultLogger, err := hooks.NewLoggingHook(false) + if err != nil { + t.Errorf("Issue setting up logger,'%s'", err) + } + + mp, err := NewMultiProvider([]UniqueNameProvider{ + { + Provider: testProvider1, + }, { + Provider: testProvider2, + }, + }, "test", defaultLogger) + + if err != nil { + t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) + } + + providerEntries := mp.Providers() + + if providerEntries[0].UniqueName != "NoopProvider" { + t.Errorf("Expected unique provider name to be: 'NoopProvider', got: '%s'", providerEntries[0].UniqueName) + } + + if providerEntries[1].UniqueName != "test2" { + t.Errorf("Expected unique provider name to be: 'test2', got: '%s'", providerEntries[1].UniqueName) + } + + if len(providerEntries) != 2 { + t.Errorf("Expected there to be 2 provider entries, got: '%d'", len(providerEntries)) + } +} + +func TestNewMultiProvider_DuplicateProviderGenerateUniqueNames(t *testing.T) { + testProvider1 := oft.NewTestProvider() + testProvider2 := oft.NewTestProvider() + testProvider3 := oft.NewTestProvider() + testProvider4 := oft.NewTestProvider() + + defaultLogger, err := hooks.NewLoggingHook(false) + if err != nil { + t.Errorf("Issue setting up logger,'%s'", err) + } + + mp, err := NewMultiProvider([]UniqueNameProvider{ + { + Provider: testProvider1, + }, { + Provider: testProvider2, + }, { + Provider: testProvider3, + }, { + Provider: testProvider4, + }, + }, "test", defaultLogger) + + if err != nil { + t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) + } + + providerEntries := mp.Providers() + + if len(providerEntries) != 4 { + t.Errorf("Expected there to be 4 provider entries, got: '%d'", len(providerEntries)) + } + + if providerEntries[0].UniqueName != "NoopProvider-1" { + t.Errorf("Expected unique provider name to be: 'NoopProvider-1', got: '%s'", providerEntries[0].UniqueName) + } + if providerEntries[1].UniqueName != "NoopProvider-2" { + t.Errorf("Expected unique provider name to be: 'NoopProvider-2', got: '%s'", providerEntries[1].UniqueName) + } + if providerEntries[2].UniqueName != "NoopProvider-3" { + t.Errorf("Expected unique provider name to be: 'NoopProvider-3', got: '%s'", providerEntries[2].UniqueName) + } + if providerEntries[3].UniqueName != "NoopProvider-4" { + t.Errorf("Expected unique provider name to be: 'NoopProvider-4', got: '%s'", providerEntries[3].UniqueName) + } + +} +func TestNewMultiProvider_ProvidersUsePassedNames(t *testing.T) { + testProvider1 := oft.NewTestProvider() + testProvider2 := oft.NewTestProvider() + + defaultLogger, err := hooks.NewLoggingHook(false) + if err != nil { + t.Errorf("Issue setting up logger,'%s'", err) + } + + mp, err := NewMultiProvider([]UniqueNameProvider{ + { + Provider: testProvider1, + UniqueName: "theFirst", + }, { + Provider: testProvider2, + UniqueName: "theSecond", + }, + }, "test", defaultLogger) + + if err != nil { + t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) + } + + providerEntries := mp.Providers() + + if providerEntries[0].UniqueName != "theFirst" { + t.Errorf("Expected unique provider name to be: 'theFirst', got: '%s'", providerEntries[0].UniqueName) + } + if providerEntries[1].UniqueName != "theSecond" { + t.Errorf("Expected unique provider name to be: 'theSecond', got: '%s'", providerEntries[1].UniqueName) + } + + if len(providerEntries) != 2 { + t.Errorf("Expected there to be 2 provider entries, got: '%d'", len(providerEntries)) + } +} + +func TestNewMultiProvider_ProvidersErrorNameNotUnique(t *testing.T) { + testProvider1 := oft.NewTestProvider() + testProvider2 := oft.NewTestProvider() + + defaultLogger, err := hooks.NewLoggingHook(false) + if err != nil { + t.Errorf("Issue setting up logger,'%s'", err) + } + + _, err = NewMultiProvider([]UniqueNameProvider{ + { + Provider: testProvider1, + UniqueName: "provider", + }, { + Provider: testProvider2, + UniqueName: "provider", + }, + }, "test", defaultLogger) + + if err == nil { + t.Errorf("Expected the multiprovider to have an error") + } + + if err.Error() != "provider names must be unique" { + t.Errorf("Expected the multiprovider to have an error of: '%s', got: '%s'", errUniqueName, err.Error()) + } +} diff --git a/release-please-config.json b/release-please-config.json index ee5b1b009..d1b4c7301 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -139,6 +139,14 @@ "bump-patch-for-minor-pre-major": true, "versioning": "default", "extra-files": [] + }, + "providers/multi-provider": { + "release-type": "go", + "package-name": "providers/multi-provider", + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "versioning": "default", + "extra-files": [] } }, "changelog-sections": [