Skip to content

fix: allow direct fetching of feature flags [IDE-1106] #322

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
55 changes: 45 additions & 10 deletions pkg/local_workflows/config_utils/feature_flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,62 @@ import (
"github.com/snyk/go-application-framework/internal/api"
"github.com/snyk/go-application-framework/pkg/configuration"
"github.com/snyk/go-application-framework/pkg/workflow"
"sync"
)

//go:generate $GOPATH/bin/mockgen -source=feature_flag.go -destination ../../mocks/feature_flag.go -package mocks -self_package github.com/snyk/go-application-framework/pkg/local_workflows/config_utils

Comment on lines +10 to +11
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice :)

func AddFeatureFlagToConfig(engine workflow.Engine, configKey string, featureFlagName string) {
config := engine.GetConfiguration()

callback := func(existingValue interface{}) (interface{}, error) {
if existingValue == nil {
httpClient := engine.GetNetworkAccess().GetHttpClient()
logger := engine.GetLogger()
url := config.GetString(configuration.API_URL)
org := config.GetString(configuration.ORGANIZATION)
apiClient := api.NewApi(url, httpClient)
result, err := apiClient.GetFeatureFlag(featureFlagName, org)
if err != nil {
logger.Printf("Failed to determine feature flag \"%s\" for org \"%s\": %s", featureFlagName, org, err)
}
return result, nil
return CurrentFeatureFlagChecker().GetFeatureFlag(engine, featureFlagName)
} else {
return existingValue, nil
}
}

config.AddDefaultValue(configKey, callback)
}

type FeatureFlagChecker interface {
GetFeatureFlag(engine workflow.Engine, featureFlagName string) (bool, error)
}

type featureFlagCheckerImpl struct {
}

var (
currentFFC FeatureFlagChecker
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like that we introduce a global variable here. Why is it needed? Can't we just have an instance in the configuration that is created when the configuration is created?

mutex = &sync.RWMutex{}
)

func CurrentFeatureFlagChecker() FeatureFlagChecker {
mutex.RLock()
defer mutex.RUnlock()
if currentFFC == nil {
currentFFC = &featureFlagCheckerImpl{}
}
return currentFFC
}

func SetCurrentFeatureFlagChecker(ffc FeatureFlagChecker) {
mutex.Lock()
defer mutex.Unlock()
currentFFC = ffc
}
Comment on lines +38 to +51
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a singleton pattern?


func (ffc *featureFlagCheckerImpl) GetFeatureFlag(engine workflow.Engine, featureFlagName string) (bool, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The singleton construct is not really used here, so I would recommend to just have a method GetFeatureFlag() without the whole singleton.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went for the overridable singleton solution like we do in

func (e *EngineImpl) SetConfiguration(config configuration.Configuration) {

because this seemed to me to be the way to allow for mocking. If there is a better way that means I can go back to just having the one function, then that would be good.

For reference, here is how I am able to use the mocking with the overridable singleton solution:
snyk/snyk-ls@e94e241#diff-29de3939a8441248dd44aa2f2eb09f23a57909cf126d85840d0a1519580e25f3R46

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe instead of a singleton the fetureFlagChecker can be set on the engine lvl engine.SetFeatureFlagChecker. or smth similar ? That way tests and other clients can set the mock.
cc @PeterSchafer

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, it seems I missed any notification on this thread. is this still something we are looking to merge?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PeterSchafer Yes, still looking to merge, but there isn't a rush for it anymore, so can be cooldown. Would you prefer I set it on the function on the engine level as Shawky suggested?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha, I see, everyone already commented on that. I think it's a bit overengineered. Having an interface for mocking is fine, but IMHO a singleton is not needed. I'd implement the interface in Configuration as it's in the configuration domain, not the engine.

config := engine.GetConfiguration()
httpClient := engine.GetNetworkAccess().GetHttpClient()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder whether it would be better to have config & httpclient injected, to decouple and avoid side-effects (same like it's done with apiClient).

logger := engine.GetLogger()
url := config.GetString(configuration.API_URL)
org := config.GetString(configuration.ORGANIZATION)
apiClient := api.NewApi(url, httpClient)
result, err := apiClient.GetFeatureFlag(featureFlagName, org)
if err != nil {
logger.Printf("Failed to determine feature flag \"%s\" for org \"%s\": %s", featureFlagName, org, err)
}
return result, nil
}
50 changes: 50 additions & 0 deletions pkg/mocks/feature_flag.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.