fix: invalid scoped-sync responses for empty flags (#1352)

Fixes an issue where invalid flag payloads were returned on sync
requests with scopes if the flag set was empty.

Below is an example of the bug.

```shell
$ grpcurl -import-path /home/todd/temp -proto sync.proto -plaintext localhost:8015 flagd.sync.v1.FlagSyncService/FetchAllFlags
{
  "flagConfiguration": "{\"flags\":{}}"
}
$ grpcurl -import-path /home/todd/temp -proto sync.proto -plaintext -d '{"selector":"../config/samples/example_flags.flagd.json"}' localhost:8015 flagd.sync.v1.FlagSyncService/FetchAllFlags 
{}
```

---------

Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
This commit is contained in:
Todd Baert 2024-07-05 14:25:34 -04:00 committed by GitHub
parent 450cbc84ca
commit 51371d25e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 59 additions and 23 deletions

View File

@ -12,6 +12,11 @@ import (
"github.com/open-feature/flagd/core/pkg/store" "github.com/open-feature/flagd/core/pkg/store"
) )
//nolint:errchkjson
var emptyConfigBytes, _ = json.Marshal(map[string]map[string]string{
"flags": {},
})
// Multiplexer abstract subscription handling and storage processing. // Multiplexer abstract subscription handling and storage processing.
// Flag configurations will be lazy loaded using reFill logic upon the calls to publish. // Flag configurations will be lazy loaded using reFill logic upon the calls to publish.
type Multiplexer struct { type Multiplexer struct {
@ -162,6 +167,10 @@ func (r *Multiplexer) SourcesAsMetadata() string {
// reFill local configuration values // reFill local configuration values
func (r *Multiplexer) reFill() error { func (r *Multiplexer) reFill() error {
clear(r.selectorFlags) clear(r.selectorFlags)
// start all sources with empty config
for _, source := range r.sources {
r.selectorFlags[source] = string(emptyConfigBytes)
}
all, err := r.store.GetAll(context.Background()) all, err := r.store.GetAll(context.Background())
if err != nil { if err != nil {
@ -170,7 +179,7 @@ func (r *Multiplexer) reFill() error {
bytes, err := json.Marshal(map[string]interface{}{"flags": all}) bytes, err := json.Marshal(map[string]interface{}{"flags": all})
if err != nil { if err != nil {
return fmt.Errorf("error from marshallin: %w", err) return fmt.Errorf("error marshalling: %w", err)
} }
r.allFlags = string(bytes) r.allFlags = string(bytes)
@ -188,6 +197,7 @@ func (r *Multiplexer) reFill() error {
} }
} }
// for all flags, sort them into their correct selector
for source, flags := range collector { for source, flags := range collector {
bytes, err := json.Marshal(map[string]interface{}{"flags": flags}) bytes, err := json.Marshal(map[string]interface{}{"flags": flags})
if err != nil { if err != nil {

View File

@ -6,8 +6,12 @@ import (
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert"
) )
const emptyConfigString = "{\"flags\":{}}"
func TestRegistration(t *testing.T) { func TestRegistration(t *testing.T) {
// given // given
mux, err := NewMux(getSimpleFlagStore()) mux, err := NewMux(getSimpleFlagStore())
@ -17,11 +21,12 @@ func TestRegistration(t *testing.T) {
} }
tests := []struct { tests := []struct {
testName string testName string
id interface{} id interface{}
source string source string
connection chan payload flagStringValidator func(flagString string, testSource string, testName string)
expectError bool connection chan payload
expectError bool
}{ }{
{ {
testName: "subscribe to all flags", testName: "subscribe to all flags",
@ -29,21 +34,38 @@ func TestRegistration(t *testing.T) {
connection: make(chan payload, 1), connection: make(chan payload, 1),
}, },
{ {
testName: "subscribe to source A", testName: "subscribe to source A",
id: context.Background(), id: context.Background(),
source: "A", source: "A",
flagStringValidator: func(flagString string, testSource string, testName string) {
assert.Contains(t, flagString, fmt.Sprintf("\"source\":\"%s\"", testSource))
},
connection: make(chan payload, 1), connection: make(chan payload, 1),
}, },
{ {
testName: "subscribe to source B", testName: "subscribe to source B",
id: context.Background(), id: context.Background(),
source: "B", source: "B",
flagStringValidator: func(flagString string, testSource string, testName string) {
assert.Contains(t, flagString, fmt.Sprintf("\"source\":\"%s\"", testSource))
},
connection: make(chan payload, 1), connection: make(chan payload, 1),
}, },
{
testName: "subscribe to empty",
id: context.Background(),
source: "C",
connection: make(chan payload, 1),
flagStringValidator: func(flagString string, testSource string, testName string) {
assert.Equal(t, flagString, emptyConfigString)
},
expectError: false,
},
{ {
testName: "subscribe to non-existing", testName: "subscribe to non-existing",
id: context.Background(), id: context.Background(),
source: "C", source: "D",
connection: make(chan payload, 1), connection: make(chan payload, 1),
expectError: true, expectError: true,
}, },
@ -75,13 +97,8 @@ func TestRegistration(t *testing.T) {
break break
} }
if initSync.flags == "" { if test.flagStringValidator != nil {
t.Fatal("expected non empty flag payload, but got empty") test.flagStringValidator(initSync.flags, test.source, test.testName)
}
// validate source of flag
if test.source != "" && !strings.Contains(initSync.flags, fmt.Sprintf("\"source\":\"%s\"", test.source)) {
t.Fatal("expected initial flag response to contain flags from source, but failed to find source")
} }
}) })
} }
@ -173,4 +190,13 @@ func TestGetAllFlags(t *testing.T) {
t.Fatal("expected flags to be scoped") t.Fatal("expected flags to be scoped")
return return
} }
// when - get all for a flagless-scope
flags, err = mux.GetAllFlags("C")
if err != nil {
t.Fatal("error when retrieving all flags")
return
}
assert.Equal(t, flags, emptyConfigString)
} }

View File

@ -128,7 +128,7 @@ func TestSyncServiceEndToEnd(t *testing.T) {
t.Fatal("expected sources entry in the metadata, but got nil") t.Fatal("expected sources entry in the metadata, but got nil")
} }
if asMap["sources"] != "A,B" { if asMap["sources"] != "A,B,C" {
t.Fatal("incorrect sources entry in metadata") t.Fatal("incorrect sources entry in metadata")
} }

View File

@ -5,7 +5,7 @@ import (
"github.com/open-feature/flagd/core/pkg/store" "github.com/open-feature/flagd/core/pkg/store"
) )
// getSimpleFlagStore returns a flag store pre-filled with flags from sources A & B // getSimpleFlagStore returns a flag store pre-filled with flags from sources A & B & C, which C empty
func getSimpleFlagStore() (*store.Flags, []string) { func getSimpleFlagStore() (*store.Flags, []string) {
variants := map[string]any{ variants := map[string]any{
"true": true, "true": true,
@ -28,5 +28,5 @@ func getSimpleFlagStore() (*store.Flags, []string) {
Source: "B", Source: "B",
}) })
return flagStore, []string{"A", "B"} return flagStore, []string{"A", "B", "C"}
} }