Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .drone.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ name: default

steps:
- name: dep
image: golang:1.17
image: golang:1.18
environment:
ACCESS_TOKEN:
from_secret: GIT_ACCESS_TOKEN
Expand All @@ -20,7 +20,7 @@ steps:
- name: gopath
path: /go
- name: check
image: golang:1.17
image: golang:1.18
environment:
ACCESS_TOKEN:
from_secret: GIT_ACCESS_TOKEN
Expand All @@ -33,7 +33,7 @@ steps:
- name: gopath
path: /go
- name: build
image: golang:1.17
image: golang:1.18
commands:
- export PATH=$PATH:$(go env GOPATH)/bin
- make build
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
############################
# STEP 1 build executable binary
############################
FROM golang:1.17 as builder
FROM golang:1.18 as builder

ARG GITHUB_ACCESS_TOKEN

Expand Down
10 changes: 5 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ all: dep generate build ## Pulls down required deps, runs required code generat

# Install oapi-codegen to generate ff server code from the apis
$(GOBIN)/oapi-codegen:
go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@v1.6.0
go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@v1.11.0

PHONY+= generate
generate: ## Generates the client for the ff-servers client service
oapi-codegen -generate client -package=client ./ff-api/docs/release/client-v1.yaml > gen/client/services.gen.go
oapi-codegen -generate types -package=client ./ff-api/docs/release/client-v1.yaml > gen/client/types.gen.go
oapi-codegen -generate client -package=admin ./ff-api/docs/release/admin-v1.yaml > gen/admin/services.gen.go
oapi-codegen -generate types -package=admin ./ff-api/docs/release/admin-v1.yaml > gen/admin/types.gen.go
oapi-codegen --config ./ff-api/config/ff-proxy/client-client.yaml ./ff-api/docs/release/client-v1.yaml > gen/client/services.gen.go
oapi-codegen --config ./ff-api/config/ff-proxy/client-types.yaml ./ff-api/docs/release/client-v1.yaml > gen/client/types.gen.go
oapi-codegen --config ./ff-api/config/ff-proxy/admin-client.yaml ./ff-api/docs/release/admin-v1.yaml > gen/admin/services.gen.go
oapi-codegen --config ./ff-api/config/ff-proxy/admin-types.yaml ./ff-api/docs/release/admin-v1.yaml > gen/admin/types.gen.go

PHONY+= build
build: ## Builds the ff-proxy service binary
Expand Down
36 changes: 35 additions & 1 deletion cache/in_mem.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,40 @@ type MemCache struct {
data map[string]map[string][]byte
}

// GetByte gets the value of a field for a given key
func (m MemCache) GetByte(ctx context.Context, key string, field string) ([]byte, error) {
m.RLock()
defer m.RUnlock()

fields, ok := m.data[key]
if !ok {
return nil, fmt.Errorf("%w: key %q doesn't exist in memcache", domain.ErrCacheNotFound, key)
}

value, ok := fields[field]
if !ok {
return nil, fmt.Errorf("%w: field %q doesn't exist in memcache for key: %q", domain.ErrCacheNotFound, field, key)
}

return value, nil
}

// SetByte sets a value in bytes in the cache for a given key and field
func (m MemCache) SetByte(ctx context.Context, key string, field string, value []byte) error {
m.Lock()
defer m.Unlock()

if v, ok := m.data[key]; ok {
v[field] = value
return nil
}

m.data[key] = map[string][]byte{
field: value,
}
return nil
}

// NewMemCache creates an initialised MemCache
func NewMemCache() MemCache {
return MemCache{&sync.RWMutex{}, map[string]map[string][]byte{}}
Expand Down Expand Up @@ -76,7 +110,7 @@ func (m MemCache) Get(ctx context.Context, key string, field string, v encoding.
}

if err := v.UnmarshalBinary(value); err != nil {
return fmt.Errorf("%w: failed to unmarshal value to %T for key: %q, field: %q", v, key, field, domain.ErrCacheInternal)
return fmt.Errorf("%v: failed to unmarshal value to %T for key: %q, field: %q", v, key, field, domain.ErrCacheInternal)
}
return nil
}
Expand Down
22 changes: 21 additions & 1 deletion cache/redis_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ func (r *RedisCache) Set(ctx context.Context, key string, field string, value en
return nil
}

// SetByte sets a value in the cache for a given key and field
func (r *RedisCache) SetByte(ctx context.Context, key string, field string, value []byte) error {
if err := r.client.HSet(ctx, key, field, value).Err(); err != nil {
return err
}
return nil
}

// SetKV sets a key and value
func (r *RedisCache) SetKV(ctx context.Context, key string, value string) error {
return r.client.Set(ctx, key, value, 0).Err()
Expand Down Expand Up @@ -60,11 +68,23 @@ func (r *RedisCache) Get(ctx context.Context, key string, field string, v encodi
}

if err := v.UnmarshalBinary(b); err != nil {
return fmt.Errorf("%w: failed to unmarshal value to %T for key: %q, field: %q", v, key, field, domain.ErrCacheInternal)
return fmt.Errorf("%v: failed to unmarshal value to %T for key: %q, field: %q", v, key, field, domain.ErrCacheInternal)
}
return nil
}

// GetByte gets the value of a field for a given key
func (r *RedisCache) GetByte(ctx context.Context, key string, field string) ([]byte, error) {
b, err := r.client.HGet(ctx, key, field).Bytes()
if err != nil {
if err == redis.Nil {
return nil, fmt.Errorf("%w: field %s doesn't exist in redis for key: %s", domain.ErrCacheNotFound, field, key)
}
return nil, err
}
return b, nil
}

// GetKV gets the value for a given key
func (r *RedisCache) GetKV(ctx context.Context, key string) (string, error) {
res, err := r.client.Get(ctx, key).Result()
Expand Down
119 changes: 75 additions & 44 deletions cache/wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ package cache
import (
"context"
"encoding"
"encoding/json"
"fmt"
"strings"
"sync"
"time"

"github.com/harness/ff-golang-server-sdk/rest"

"github.com/harness/ff-golang-server-sdk/dto"
"github.com/harness/ff-golang-server-sdk/evaluation"
"github.com/harness/ff-golang-server-sdk/logger"
"github.com/harness/ff-proxy/domain"
"github.com/harness/ff-proxy/log"
Expand All @@ -25,8 +28,12 @@ import (
type Cache interface {
// Set sets a value in the cache for a given key and field
Set(ctx context.Context, key string, field string, value encoding.BinaryMarshaler) error
// SetByte sets a value in the cache for a given key and field
SetByte(ctx context.Context, key string, field string, value []byte) error
// Get gets the value of a field for a given key
Get(ctx context.Context, key string, field string, v encoding.BinaryUnmarshaler) error
// GetByte gets the value of a field for a given key
GetByte(ctx context.Context, key string, field string) ([]byte, error)
// GetAll gets all of the fiels and their values for a given key
GetAll(ctx context.Context, key string) (map[string][]byte, error)
// RemoveAll removes all the fields and their values for a given key
Expand Down Expand Up @@ -93,13 +100,36 @@ func (wrapper *Wrapper) Set(key interface{}, value interface{}) (evicted bool) {
return
}

domainValue, err := wrapper.convertEvaluationToDomain(cacheKey.kind, value)
if err != nil {
wrapper.logger.Error("failed to convert Evaluation object to Domain object", "err", err)
var val []byte
switch cacheKey.kind {
case dto.KeySegment:
segmentConfig, ok := value.(rest.Segment)
if !ok {
wrapper.logger.Error("failed to cast value in cache to rest.Segment")
return
}
val, err = json.Marshal(segmentConfig)
if err != nil {
wrapper.logger.Error("failed to marshal segmentConfig", "err", err)
return
}
case dto.KeyFeature:
featureConfig, ok := value.(rest.FeatureConfig)
if !ok {
wrapper.logger.Error("failed to cast value in cache to rest.FeatureConfig")
return
}
val, err = json.Marshal(featureConfig)
if err != nil {
wrapper.logger.Error("failed to marshal featureConfig", "err", err)
return
}
default:
wrapper.logger.Error("unexpected type trying to be set")
return
}

err = wrapper.cache.Set(context.Background(), cacheKey.name, cacheKey.field, domainValue)
err = wrapper.cache.SetByte(context.Background(), cacheKey.name, cacheKey.field, val)
if err != nil {
wrapper.logger.Warn("failed to set key to wrapper cache", "err", err)
return
Expand Down Expand Up @@ -214,9 +244,9 @@ func (wrapper *Wrapper) Resize(size int) (evicted int) {
*/

func (wrapper *Wrapper) decodeDTOKey(key interface{}) (cacheKey, error) {
// decode key
dtoKey, ok := key.(dto.Key)
if !ok {

dtoKey, err := convertToDTOKey(key)
if err != nil {
return cacheKey{}, fmt.Errorf("couldn't convert key to dto.Key: %s", key)
}

Expand Down Expand Up @@ -244,27 +274,6 @@ func (wrapper *Wrapper) generateKeyName(keyType string) (string, error) {
}
}

// convertEvaluationToDomain converts the data being cached by the sdk to it's appropriate internal type i.e. domain.FeatureFlag
func (wrapper *Wrapper) convertEvaluationToDomain(keyType string, value interface{}) (encoding.BinaryMarshaler, error) {
switch keyType {
case dto.KeyFeature:
featureConfig, ok := value.(evaluation.FeatureConfig)
if !ok {
return &domain.FeatureFlag{}, fmt.Errorf("couldn't convert to evaluation.FeatureFlag")
}

return domain.ConvertEvaluationFeatureConfig(featureConfig), nil
case dto.KeySegment:
segmentConfig, ok := value.(evaluation.Segment)
if !ok {
return &domain.Segment{}, fmt.Errorf("couldn't convert to evaluation.Segment")
}
return domain.ConvertEvaluationSegment(segmentConfig), nil
default:
return nil, fmt.Errorf("key type not recognised: %s", keyType)
}
}

func (wrapper *Wrapper) getKeysByType(keyType string) []interface{} {
wrapper.logger = wrapper.logger.With("method", "getKeysByType", "keyType", keyType)

Expand Down Expand Up @@ -317,33 +326,55 @@ func (wrapper *Wrapper) get(key cacheKey) (interface{}, error) {
}

func (wrapper *Wrapper) getFeatureConfig(key cacheKey) (interface{}, error) {
var val encoding.BinaryUnmarshaler = &domain.FeatureConfig{}
// get FeatureFlag in domain.FeatureFlag format
err := wrapper.cache.Get(context.Background(), key.name, key.field, val)
// get FeatureFlag in rest.FeatureConfig format
var featureConfig = rest.FeatureConfig{}
val, err := wrapper.cache.GetByte(context.Background(), key.name, key.field)
if err != nil {
return nil, err
}
featureConfig, ok := val.(*domain.FeatureConfig)
if !ok {
return nil, fmt.Errorf("couldn't cast cached value to domain.FeatureFlag: %s", val)
}

// return to sdk in evaluation.FeatureFlag format
return *domain.ConvertDomainFeatureConfig(*featureConfig), nil
err = json.Unmarshal(val, &featureConfig)
if err != nil {
return nil, fmt.Errorf("couldn't cast cached value to rest.FeatureConfig: %s", val)
}
// return to sdk in rest.FeatureConfig format
return featureConfig, nil
}

func (wrapper *Wrapper) getSegment(key cacheKey) (interface{}, error) {
var val encoding.BinaryUnmarshaler = &domain.Segment{}
var segment = rest.Segment{}
// get Segment in domain.Segment format
err := wrapper.cache.Get(context.Background(), key.name, key.field, val)
val, err := wrapper.cache.GetByte(context.Background(), key.name, key.field)
if err != nil {
return nil, err
}
segment, ok := val.(*domain.Segment)
if !ok {
return nil, fmt.Errorf("couldn't cast cached value to domain.Segment: %s", val)

err = json.Unmarshal(val, &segment)
if err != nil {
return nil, fmt.Errorf("couldn't cast cached value to rest.Segment: %s", val)
}

// return to sdk in evaluation.Segment format
return domain.ConvertDomainSegment(*segment), nil
return segment, nil
}

func convertToDTOKey(key interface{}) (dto.Key, error) {
myKey, ok := key.(string)
if !ok {
dtoKey, ok := key.(dto.Key)
if !ok {
return dto.Key{}, fmt.Errorf("couldn't convert key to dto.Key: %s", key)
}
return dtoKey, nil
}

keyArr := strings.SplitN(myKey, "/", 2)
if len(keyArr) != 2 {
return dto.Key{}, fmt.Errorf("couldn't convert key to dto.Key: %s", key)
}
dtoKey := dto.Key{
Type: keyArr[0],
Name: keyArr[1],
}
return dtoKey, nil
}
Loading