From 8377d3a8b51a236d0152dcaa98594c8d14419979 Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Fri, 9 Oct 2020 10:00:01 +0200 Subject: [PATCH] Evaluation with FlagTagsOperator to match any or all tags --- docs/api_docs/bundle.yaml | 22 +++++++ integration_tests/test.sh | 24 ++++++++ pkg/handler/eval.go | 15 ++--- pkg/handler/eval_cache.go | 59 +++++++++++++++++-- pkg/handler/eval_cache_test.go | 21 ++++++- swagger/index.yaml | 20 +++++++ swagger_gen/models/eval_context.go | 53 +++++++++++++++++ .../models/evaluation_batch_request.go | 52 ++++++++++++++++ swagger_gen/restapi/embedded_spec.go | 36 +++++++++++ 9 files changed, 289 insertions(+), 13 deletions(-) diff --git a/docs/api_docs/bundle.yaml b/docs/api_docs/bundle.yaml index 72c99a49..69483c4f 100644 --- a/docs/api_docs/bundle.yaml +++ b/docs/api_docs/bundle.yaml @@ -1311,6 +1311,17 @@ definitions: x-omitempty: true items: type: string + flagTagsOperator: + description: >- + determine how flagTags is used to filter flags to be evaluated. OR + extends the evaluation to those which contains at least one of the + provided flagTags or AND limit the evaluation to those which contains + all the flagTags. + type: string + enum: + - ANY + - ALL + default: ANY evalResult: type: object properties: @@ -1403,6 +1414,17 @@ definitions: type: string minLength: 1 minItems: 1 + flagTagsOperator: + description: >- + determine how flagTags is used to filter flags to be evaluated. OR + extends the evaluation to those which contains at least one of the + provided flagTags or AND limit the evaluation to those which contains + all the flagTags. + type: string + enum: + - ANY + - ALL + default: ANY evaluationBatchResponse: type: object required: diff --git a/integration_tests/test.sh b/integration_tests/test.sh index da351898..0d596a89 100644 --- a/integration_tests/test.sh +++ b/integration_tests/test.sh @@ -365,6 +365,29 @@ step_11_test_tag_batch_evaluation() } +step_12_test_tag_operator_batch_evaluation() +{ + flagr_url=$1:18000/api/v1 + sleep 5 + + shakedown POST $flagr_url/evaluation/batch -H 'Content-Type:application/json' -d '{"entities":[{ "entityType": "externalalert", "entityContext": {"property_1": "value_2"} }],"flagTags": ["value_1", "value_2"], "flagTagsOperator": "ALL", "enableDebug": false }' + status 200 + matches "\"flagID\":1" + matches "\"variantKey\":\"key_1\"" + matches "\"variantID\":1" + + shakedown POST $flagr_url/evaluation/batch -H 'Content-Type:application/json' -d '{"entities":[{ "entityType": "externalalert", "entityContext": {"property_1": "value_2"} }],"flagTags": ["value_1", "value_3"], "flagTagsOperator": "ALL", "enableDebug": false }' + status 200 + contains "\"evaluationResults\":null" + + shakedown POST $flagr_url/evaluation/batch -H 'Content-Type:application/json' -d '{"entities":[{ "entityType": "externalalert", "entityContext": {"property_1": "value_2"} }],"flagTags": ["value_1", "value_3"], "flagTagsOperator": "ANY", "enableDebug": false }' + status 200 + matches "\"flagID\":1" + matches "\"variantKey\":\"key_1\"" + matches "\"variantID\":1" + +} + start_test() { @@ -387,6 +410,7 @@ start_test() step_9_test_export $flagr_host step_10_test_crud_tag $flagr_host step_11_test_tag_batch_evaluation $flagr_host + step_12_test_tag_operator_batch_evaluation $flagr_host } start(){ diff --git a/pkg/handler/eval.go b/pkg/handler/eval.go index 19eeb2ae..fa67ee53 100644 --- a/pkg/handler/eval.go +++ b/pkg/handler/eval.go @@ -51,17 +51,19 @@ func (e *eval) PostEvaluationBatch(params evaluation.PostEvaluationBatchParams) flagIDs := params.Body.FlagIDs flagKeys := params.Body.FlagKeys flagTags := params.Body.FlagTags + flagTagsOperator := params.Body.FlagTagsOperator results := &models.EvaluationBatchResponse{} // TODO make it concurrent for _, entity := range entities { if len(flagTags) > 0 { evalContext := models.EvalContext{ - EnableDebug: params.Body.EnableDebug, - EntityContext: entity.EntityContext, - EntityID: entity.EntityID, - EntityType: entity.EntityType, - FlagTags: flagTags, + EnableDebug: params.Body.EnableDebug, + EntityContext: entity.EntityContext, + EntityID: entity.EntityID, + EntityType: entity.EntityType, + FlagTags: flagTags, + FlagTagsOperator: flagTagsOperator, } evalResults := EvalFlagsByTags(evalContext) results.EvaluationResults = append(results.EvaluationResults, evalResults...) @@ -133,8 +135,7 @@ var LookupFlag = func(evalContext models.EvalContext) *entity.Flag { var EvalFlagsByTags = func(evalContext models.EvalContext) []*models.EvalResult { cache := GetEvalCache() - - fs := cache.GetByTags(evalContext.FlagTags) + fs := cache.GetByTags(evalContext.FlagTags, evalContext.FlagTagsOperator) results := []*models.EvalResult{} for _, f := range fs { results = append(results, EvalFlagWithContext(f, evalContext)) diff --git a/pkg/handler/eval_cache.go b/pkg/handler/eval_cache.go index ef6a97c8..9a375c7e 100644 --- a/pkg/handler/eval_cache.go +++ b/pkg/handler/eval_cache.go @@ -5,6 +5,8 @@ import ( "sync/atomic" "time" + "github.com/checkr/flagr/swagger_gen/models" + "github.com/checkr/flagr/pkg/config" "github.com/checkr/flagr/pkg/entity" "github.com/checkr/flagr/pkg/util" @@ -59,9 +61,29 @@ func (ec *EvalCache) Start() { }() } -func (ec *EvalCache) GetByTags(tags []string) []*entity.Flag { +func (ec *EvalCache) GetByTags(tags []string, operator *string) []*entity.Flag { + var results map[uint]*entity.Flag + + if operator == nil || *operator == models.EvaluationBatchRequestFlagTagsOperatorANY { + results = ec.getByTagsANY(tags) + } + + if operator != nil && *operator == models.EvaluationBatchRequestFlagTagsOperatorALL { + results = ec.getByTagsALL(tags) + } + + values := make([]*entity.Flag, 0, len(results)) + for _, f := range results { + values = append(values, f) + } + + return values +} + +func (ec *EvalCache) getByTagsANY(tags []string) map[uint]*entity.Flag { results := map[uint]*entity.Flag{} cache := ec.cache.Load().(*cacheContainer) + for _, t := range tags { fSet, ok := cache.tagCache[t] if ok { @@ -70,13 +92,40 @@ func (ec *EvalCache) GetByTags(tags []string) []*entity.Flag { } } } + return results +} - values := make([]*entity.Flag, 0, len(results)) - for _, f := range results { - values = append(values, f) +func (ec *EvalCache) getByTagsALL(tags []string) map[uint]*entity.Flag { + results := map[uint]*entity.Flag{} + cache := ec.cache.Load().(*cacheContainer) + + for i, t := range tags { + fSet, ok := cache.tagCache[t] + if !ok { + // no flags + return map[uint]*entity.Flag{} + } + + if i == 0 { + // store all the flags + for fID, f := range fSet { + results[fID] = f + } + } else { + for fID := range results { + if _, ok := fSet[fID]; !ok { + delete(results, fID) + } + } + + // no flags left + if len(results) == 0 { + return results + } + } } - return values + return results } // GetByFlagKeyOrID gets the flag by Key or ID diff --git a/pkg/handler/eval_cache_test.go b/pkg/handler/eval_cache_test.go index 99131ba4..f7e61ae7 100644 --- a/pkg/handler/eval_cache_test.go +++ b/pkg/handler/eval_cache_test.go @@ -1,6 +1,7 @@ package handler import ( + "github.com/checkr/flagr/swagger_gen/models" "testing" "github.com/checkr/flagr/pkg/entity" @@ -35,8 +36,26 @@ func TestGetByTags(t *testing.T) { for i, s := range fixtureFlag.Tags { tags[i] = s.Value } - f := ec.GetByTags(tags) + any := models.EvalContextFlagTagsOperatorANY + all := models.EvalContextFlagTagsOperatorALL + f := ec.GetByTags(tags, &any) assert.Len(t, f, 1) assert.Equal(t, f[0].ID, fixtureFlag.ID) assert.Equal(t, f[0].Tags[0].Value, fixtureFlag.Tags[0].Value) + + tags = make([]string, len(fixtureFlag.Tags)+1) + for i, s := range fixtureFlag.Tags { + tags[i] = s.Value + } + tags[len(tags)-1] = "tag3" + + f = ec.GetByTags(tags, &any) + assert.Len(t, f, 1) + + var operator *string + f = ec.GetByTags(tags, operator) + assert.Len(t, f, 1) + + f = ec.GetByTags(tags, &all) + assert.Len(t, f, 0) } diff --git a/swagger/index.yaml b/swagger/index.yaml index 385722ce..cbeab216 100644 --- a/swagger/index.yaml +++ b/swagger/index.yaml @@ -459,6 +459,16 @@ definitions: x-omitempty: true items: type: string + flagTagsOperator: + description: >- + determine how flagTags is used to filter flags to be evaluated. OR extends the evaluation to those which + contains at least one of the provided flagTags or AND limit the evaluation to those which contains all the + flagTags. + type: string + enum: + - "ANY" + - "ALL" + default: "ANY" evalResult: type: object properties: @@ -549,6 +559,16 @@ definitions: type: string minLength: 1 minItems: 1 + flagTagsOperator: + description: >- + determine how flagTags is used to filter flags to be evaluated. OR extends the evaluation to those which + contains at least one of the provided flagTags or AND limit the evaluation to those which contains all the + flagTags. + type: string + enum: + - "ANY" + - "ALL" + default: "ANY" evaluationBatchResponse: type: object required: diff --git a/swagger_gen/models/eval_context.go b/swagger_gen/models/eval_context.go index 3cc41a09..bc823774 100644 --- a/swagger_gen/models/eval_context.go +++ b/swagger_gen/models/eval_context.go @@ -6,6 +6,8 @@ package models // Editing this file might prove futile when you re-run the swagger generate command import ( + "encoding/json" + "github.com/go-openapi/errors" "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" @@ -38,6 +40,10 @@ type EvalContext struct { // flagTags. flagTags looks up flags by tag. Either works. FlagTags []string `json:"flagTags,omitempty"` + + // determine how flagTags is used to filter flags to be evaluated. OR extends the evaluation to those which contains at least one of the provided flagTags or AND limit the evaluation to those which contains all the flagTags. + // Enum: [ANY ALL] + FlagTagsOperator *string `json:"flagTagsOperator,omitempty"` } // Validate validates this eval context @@ -48,6 +54,10 @@ func (m *EvalContext) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateFlagTagsOperator(formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { return errors.CompositeValidationError(res...) } @@ -67,6 +77,49 @@ func (m *EvalContext) validateFlagID(formats strfmt.Registry) error { return nil } +var evalContextTypeFlagTagsOperatorPropEnum []interface{} + +func init() { + var res []string + if err := json.Unmarshal([]byte(`["ANY","ALL"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + evalContextTypeFlagTagsOperatorPropEnum = append(evalContextTypeFlagTagsOperatorPropEnum, v) + } +} + +const ( + + // EvalContextFlagTagsOperatorANY captures enum value "ANY" + EvalContextFlagTagsOperatorANY string = "ANY" + + // EvalContextFlagTagsOperatorALL captures enum value "ALL" + EvalContextFlagTagsOperatorALL string = "ALL" +) + +// prop value enum +func (m *EvalContext) validateFlagTagsOperatorEnum(path, location string, value string) error { + if err := validate.EnumCase(path, location, value, evalContextTypeFlagTagsOperatorPropEnum, true); err != nil { + return err + } + return nil +} + +func (m *EvalContext) validateFlagTagsOperator(formats strfmt.Registry) error { + + if swag.IsZero(m.FlagTagsOperator) { // not required + return nil + } + + // value enum + if err := m.validateFlagTagsOperatorEnum("flagTagsOperator", "body", *m.FlagTagsOperator); err != nil { + return err + } + + return nil +} + // MarshalBinary interface implementation func (m *EvalContext) MarshalBinary() ([]byte, error) { if m == nil { diff --git a/swagger_gen/models/evaluation_batch_request.go b/swagger_gen/models/evaluation_batch_request.go index a1fd6927..dc1e6fa6 100644 --- a/swagger_gen/models/evaluation_batch_request.go +++ b/swagger_gen/models/evaluation_batch_request.go @@ -6,6 +6,7 @@ package models // Editing this file might prove futile when you re-run the swagger generate command import ( + "encoding/json" "strconv" "github.com/go-openapi/errors" @@ -38,6 +39,10 @@ type EvaluationBatchRequest struct { // flagTags. Either flagIDs, flagKeys or flagTags works. If pass in multiples, Flagr may return duplicate results. // Min Items: 1 FlagTags []string `json:"flagTags"` + + // determine how flagTags is used to filter flags to be evaluated. OR extends the evaluation to those which contains at least one of the provided flagTags or AND limit the evaluation to those which contains all the flagTags. + // Enum: [ANY ALL] + FlagTagsOperator *string `json:"flagTagsOperator,omitempty"` } // Validate validates this evaluation batch request @@ -60,6 +65,10 @@ func (m *EvaluationBatchRequest) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateFlagTagsOperator(formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { return errors.CompositeValidationError(res...) } @@ -166,6 +175,49 @@ func (m *EvaluationBatchRequest) validateFlagTags(formats strfmt.Registry) error return nil } +var evaluationBatchRequestTypeFlagTagsOperatorPropEnum []interface{} + +func init() { + var res []string + if err := json.Unmarshal([]byte(`["ANY","ALL"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + evaluationBatchRequestTypeFlagTagsOperatorPropEnum = append(evaluationBatchRequestTypeFlagTagsOperatorPropEnum, v) + } +} + +const ( + + // EvaluationBatchRequestFlagTagsOperatorANY captures enum value "ANY" + EvaluationBatchRequestFlagTagsOperatorANY string = "ANY" + + // EvaluationBatchRequestFlagTagsOperatorALL captures enum value "ALL" + EvaluationBatchRequestFlagTagsOperatorALL string = "ALL" +) + +// prop value enum +func (m *EvaluationBatchRequest) validateFlagTagsOperatorEnum(path, location string, value string) error { + if err := validate.EnumCase(path, location, value, evaluationBatchRequestTypeFlagTagsOperatorPropEnum, true); err != nil { + return err + } + return nil +} + +func (m *EvaluationBatchRequest) validateFlagTagsOperator(formats strfmt.Registry) error { + + if swag.IsZero(m.FlagTagsOperator) { // not required + return nil + } + + // value enum + if err := m.validateFlagTagsOperatorEnum("flagTagsOperator", "body", *m.FlagTagsOperator); err != nil { + return err + } + + return nil +} + // MarshalBinary interface implementation func (m *EvaluationBatchRequest) MarshalBinary() ([]byte, error) { if m == nil { diff --git a/swagger_gen/restapi/embedded_spec.go b/swagger_gen/restapi/embedded_spec.go index b4b8395f..5c51414c 100644 --- a/swagger_gen/restapi/embedded_spec.go +++ b/swagger_gen/restapi/embedded_spec.go @@ -1569,6 +1569,15 @@ func init() { "type": "string" }, "x-omitempty": true + }, + "flagTagsOperator": { + "description": "determine how flagTags is used to filter flags to be evaluated. OR extends the evaluation to those which contains at least one of the provided flagTags or AND limit the evaluation to those which contains all the flagTags.", + "type": "string", + "default": "ANY", + "enum": [ + "ANY", + "ALL" + ] } } }, @@ -1668,6 +1677,15 @@ func init() { "type": "string", "minLength": 1 } + }, + "flagTagsOperator": { + "description": "determine how flagTags is used to filter flags to be evaluated. OR extends the evaluation to those which contains at least one of the provided flagTags or AND limit the evaluation to those which contains all the flagTags.", + "type": "string", + "default": "ANY", + "enum": [ + "ANY", + "ALL" + ] } } }, @@ -3617,6 +3635,15 @@ func init() { "type": "string" }, "x-omitempty": true + }, + "flagTagsOperator": { + "description": "determine how flagTags is used to filter flags to be evaluated. OR extends the evaluation to those which contains at least one of the provided flagTags or AND limit the evaluation to those which contains all the flagTags.", + "type": "string", + "default": "ANY", + "enum": [ + "ANY", + "ALL" + ] } } }, @@ -3716,6 +3743,15 @@ func init() { "type": "string", "minLength": 1 } + }, + "flagTagsOperator": { + "description": "determine how flagTags is used to filter flags to be evaluated. OR extends the evaluation to those which contains at least one of the provided flagTags or AND limit the evaluation to those which contains all the flagTags.", + "type": "string", + "default": "ANY", + "enum": [ + "ANY", + "ALL" + ] } } },