Skip to content

Commit

Permalink
feat: add single evaluation endpoint for OFREP (#3267)
Browse files Browse the repository at this point in the history
* feat: add single evaluation endpoint for OFREP

Signed-off-by: Pablo Aguilar <pablo.aguilar@outlook.com.br>

* chore: return an empty metadata

Signed-off-by: Pablo Aguilar <pablo.aguilar@outlook.com.br>

* fix: missing body declaration in proto config

Signed-off-by: Pablo Aguilar <pablo.aguilar@outlook.com.br>

* chore: support targetingKey

Signed-off-by: Pablo Aguilar <pablo.aguilar@outlook.com.br>

---------

Signed-off-by: Pablo Aguilar <pablo.aguilar@outlook.com.br>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
thepabloaguilar and kodiakhq[bot] committed Jul 29, 2024
1 parent fa8f302 commit 9d25c18
Show file tree
Hide file tree
Showing 15 changed files with 1,076 additions and 67 deletions.
1 change: 1 addition & 0 deletions go.work.sum
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,7 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d/go.
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ func NewGRPCServer(
evalsrv = evaluation.New(logger, store)
evaldatasrv = evaluationdata.New(logger, store)
healthsrv = health.NewServer()
ofrepsrv = ofrep.New(cfg.Cache)
ofrepsrv = ofrep.New(logger, cfg.Cache, evalsrv)
)

var (
Expand Down
90 changes: 90 additions & 0 deletions internal/server/evaluation/ofrep_bridge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package evaluation

import (
"context"
"errors"
"strconv"

"github.com/google/uuid"

"go.flipt.io/flipt/internal/server/ofrep"

rpcevaluation "go.flipt.io/flipt/rpc/flipt/evaluation"

fliptotel "go.flipt.io/flipt/internal/server/otel"
"go.flipt.io/flipt/internal/storage"
"go.flipt.io/flipt/rpc/flipt"
"go.opentelemetry.io/otel/trace"
)

const ofrepCtxTargetingKey = "targetingKey"

func (s *Server) OFREPEvaluationBridge(ctx context.Context, input ofrep.EvaluationBridgeInput) (ofrep.EvaluationBridgeOutput, error) {
flag, err := s.store.GetFlag(ctx, storage.NewResource(input.NamespaceKey, input.FlagKey))
if err != nil {
return ofrep.EvaluationBridgeOutput{}, err
}

span := trace.SpanFromContext(ctx)
span.SetAttributes(
fliptotel.AttributeNamespace.String(input.NamespaceKey),
fliptotel.AttributeFlag.String(input.FlagKey),
fliptotel.AttributeProviderName,
)

req := &rpcevaluation.EvaluationRequest{
NamespaceKey: input.NamespaceKey,
FlagKey: input.FlagKey,
EntityId: uuid.NewString(),
Context: input.Context,
}

// https://openfeature.dev/docs/reference/concepts/evaluation-context/#targeting-key
if targetingKey, ok := input.Context[ofrepCtxTargetingKey]; ok {
req.EntityId = targetingKey
}

switch flag.Type {
case flipt.FlagType_VARIANT_FLAG_TYPE:
resp, err := s.variant(ctx, flag, req)
if err != nil {
return ofrep.EvaluationBridgeOutput{}, err
}

span.SetAttributes(
fliptotel.AttributeMatch.Bool(resp.Match),
fliptotel.AttributeValue.String(resp.VariantKey),
fliptotel.AttributeReason.String(resp.Reason.String()),
fliptotel.AttributeSegments.StringSlice(resp.SegmentKeys),
fliptotel.AttributeFlagKey(resp.FlagKey),
fliptotel.AttributeFlagVariant(resp.VariantKey),
)

return ofrep.EvaluationBridgeOutput{
FlagKey: resp.FlagKey,
Reason: resp.Reason,
Variant: resp.VariantKey,
Value: resp.VariantKey,
}, nil
case flipt.FlagType_BOOLEAN_FLAG_TYPE:
resp, err := s.boolean(ctx, flag, req)
if err != nil {
return ofrep.EvaluationBridgeOutput{}, err
}

span.SetAttributes(
fliptotel.AttributeValue.Bool(resp.Enabled),
fliptotel.AttributeReason.String(resp.Reason.String()),
fliptotel.AttributeFlagVariant(strconv.FormatBool(resp.Enabled)),
)

return ofrep.EvaluationBridgeOutput{
FlagKey: resp.FlagKey,
Variant: strconv.FormatBool(resp.Enabled),
Reason: resp.Reason,
Value: resp.Enabled,
}, nil
default:
return ofrep.EvaluationBridgeOutput{}, errors.New("unsupported flag type for ofrep bridge")
}
}
106 changes: 106 additions & 0 deletions internal/server/evaluation/ofrep_bridge_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package evaluation

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"go.flipt.io/flipt/internal/server/ofrep"
"go.flipt.io/flipt/internal/storage"
"go.flipt.io/flipt/rpc/flipt"
rpcevaluation "go.flipt.io/flipt/rpc/flipt/evaluation"
"go.uber.org/zap/zaptest"
)

func TestOFREPEvaluationBridge_Variant(t *testing.T) {
var (
flagKey = "test-flag"
namespaceKey = "test-namespace"
store = &evaluationStoreMock{}
logger = zaptest.NewLogger(t)
s = New(logger, store)
flag = &flipt.Flag{
NamespaceKey: namespaceKey,
Key: flagKey,
Enabled: true,
Type: flipt.FlagType_VARIANT_FLAG_TYPE,
}
)

store.On("GetFlag", mock.Anything, storage.NewResource(namespaceKey, flagKey)).Return(flag, nil)

store.On("GetEvaluationRules", mock.Anything, storage.NewResource(namespaceKey, flagKey)).Return([]*storage.EvaluationRule{
{
ID: "1",
FlagKey: flagKey,
Rank: 0,
Segments: map[string]*storage.EvaluationSegment{
"bar": {
SegmentKey: "bar",
MatchType: flipt.MatchType_ANY_MATCH_TYPE,
},
},
},
}, nil)

store.On("GetEvaluationDistributions", mock.Anything, storage.NewID("1")).Return([]*storage.EvaluationDistribution{
{
ID: "4",
RuleID: "1",
VariantID: "5",
Rollout: 100,
VariantKey: "boz",
},
}, nil)

output, err := s.OFREPEvaluationBridge(context.TODO(), ofrep.EvaluationBridgeInput{
FlagKey: flagKey,
NamespaceKey: namespaceKey,
Context: map[string]string{
"hello": "world",
"targetingKey": "12345",
},
})
require.NoError(t, err)

assert.Equal(t, flagKey, output.FlagKey)
assert.Equal(t, rpcevaluation.EvaluationReason_MATCH_EVALUATION_REASON, output.Reason)
assert.Equal(t, "boz", output.Variant)
assert.Equal(t, "boz", output.Value)
}

func TestOFREPEvaluationBridge_Boolean(t *testing.T) {
var (
flagKey = "test-flag"
namespaceKey = "test-namespace"
store = &evaluationStoreMock{}
logger = zaptest.NewLogger(t)
s = New(logger, store)
flag = &flipt.Flag{
NamespaceKey: namespaceKey,
Key: flagKey,
Enabled: true,
Type: flipt.FlagType_BOOLEAN_FLAG_TYPE,
}
)

store.On("GetFlag", mock.Anything, storage.NewResource(namespaceKey, flagKey)).Return(flag, nil)

store.On("GetEvaluationRollouts", mock.Anything, storage.NewResource(namespaceKey, flagKey)).Return([]*storage.EvaluationRollout{}, nil)

output, err := s.OFREPEvaluationBridge(context.TODO(), ofrep.EvaluationBridgeInput{
FlagKey: flagKey,
NamespaceKey: namespaceKey,
Context: map[string]string{
"targetingKey": "12345",
},
})
require.NoError(t, err)

assert.Equal(t, flagKey, output.FlagKey)
assert.Equal(t, rpcevaluation.EvaluationReason_DEFAULT_EVALUATION_REASON, output.Reason)
assert.Equal(t, "true", output.Variant)
assert.Equal(t, true, output.Value)
}
18 changes: 18 additions & 0 deletions internal/server/ofrep/bridge_mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package ofrep

import (
"context"

"github.com/stretchr/testify/mock"
)

var _ Bridge = &bridgeMock{}

type bridgeMock struct {
mock.Mock
}

func (b *bridgeMock) OFREPEvaluationBridge(ctx context.Context, input EvaluationBridgeInput) (EvaluationBridgeOutput, error) {
args := b.Called(ctx, input)
return args.Get(0).(EvaluationBridgeOutput), args.Error(1)
}
88 changes: 88 additions & 0 deletions internal/server/ofrep/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package ofrep

import (
"encoding/json"
"fmt"
"strings"

"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

type errorCode string

const (
errorCodeFlagNotFound errorCode = "FLAG_NOT_FOUND"
errorCodeParseError errorCode = "PARSE_ERROR"
errorCodeTargetingKeyMissing errorCode = "TARGETING_KEY_MISSING"
errorCodeInvalidContext errorCode = "INVALID_CONTEXT"
errorCodeParseGeneral errorCode = "GENERAL"
)

type errorSchema struct {
Key string `json:"key,omitempty"`
ErrorCode string `json:"errorCode,omitempty"`
ErrorDetails string `json:"errorDetails"`
}

func NewBadRequestError(key string, err error) error {
msg, err := json.Marshal(errorSchema{
Key: key,
ErrorCode: string(errorCodeParseGeneral),
ErrorDetails: err.Error(),
})
if err != nil {
return NewInternalServerError(err)
}

return status.Error(codes.InvalidArgument, string(msg))
}

func NewUnauthenticatedError() error {
msg, err := json.Marshal(errorSchema{ErrorDetails: "unauthenticated error"})
if err != nil {
return NewInternalServerError(err)
}

return status.Error(codes.Unauthenticated, string(msg))
}

func NewUnauthorizedError() error {
msg, err := json.Marshal(errorSchema{ErrorDetails: "unauthorized error"})
if err != nil {
return NewInternalServerError(err)
}

return status.Error(codes.PermissionDenied, string(msg))
}

func NewFlagNotFoundError(key string) error {
msg, err := json.Marshal(errorSchema{
Key: key,
ErrorCode: string(errorCodeFlagNotFound),
})
if err != nil {
return NewInternalServerError(err)
}

return status.Error(codes.NotFound, string(msg))
}

func NewTargetingKeyMissing() error {
msg, err := json.Marshal(errorSchema{
ErrorCode: string(errorCodeTargetingKeyMissing),
ErrorDetails: "flag key was not provided",
})
if err != nil {
return NewInternalServerError(err)
}

return status.Error(codes.InvalidArgument, string(msg))
}

func NewInternalServerError(err error) error {
return status.Error(
codes.Internal,
fmt.Sprintf(`{"errorDetails": "%s"}`, strings.ReplaceAll(err.Error(), `"`, `\"`)),
)
}
Loading

0 comments on commit 9d25c18

Please sign in to comment.