diff --git a/cmd/ff-proxy/main.go b/cmd/ff-proxy/main.go index d67060fc..5e410f98 100644 --- a/cmd/ff-proxy/main.go +++ b/cmd/ff-proxy/main.go @@ -108,6 +108,7 @@ var ( flagPollInterval int flagStreamEnabled bool generateOfflineConfig bool + configDir string ) const ( @@ -133,6 +134,7 @@ const ( flagPollIntervalEnv = "FLAG_POLL_INTERVAL" flagStreamEnabledEnv = "FLAG_STREAM_ENABLED" generateOfflineConfigEnv = "GENERATE_OFFLINE_CONFIG" + configDirEnv = "CONFIG_DIR" pprofEnabledEnv = "PPROF" bypassAuthFlag = "bypass-auth" @@ -157,6 +159,7 @@ const ( pprofEnabledFlag = "pprof" flagStreamEnabledFlag = "flag-stream-enabled" generateOfflineConfigFlag = "generate-offline-config" + configDirFlag = "config-dir" flagPollIntervalFlag = "flag-poll-interval" ) @@ -185,6 +188,7 @@ func init() { flag.IntVar(&flagPollInterval, flagPollIntervalFlag, 1, "how often in minutes the proxy should poll for flag updates (if stream not connected)") flag.BoolVar(&flagStreamEnabled, flagStreamEnabledFlag, true, "should the proxy connect to Harness in streaming mode to get flag changes") flag.BoolVar(&generateOfflineConfig, generateOfflineConfigFlag, false, "if true the proxy will produce offline config in the /config directory then terminate") + flag.StringVar(&configDir, configDirFlag, "/config", "specify a custom path to search for the offline config directory. Defaults to /config") sdkClients = newSDKClientMap() loadFlagsFromEnv(map[string]string{ @@ -210,6 +214,7 @@ func init() { pprofEnabledEnv: pprofEnabledFlag, flagStreamEnabledEnv: flagStreamEnabledFlag, generateOfflineConfigEnv: generateOfflineConfigFlag, + configDirEnv: configDirFlag, flagPollIntervalEnv: flagPollIntervalFlag, }) @@ -297,7 +302,7 @@ func main() { cancel() }() - logger.Info("service config", "pprof", pprofEnabled, "debug", debug, "bypass-auth", bypassAuth, "offline", offline, "port", port, "admin-service", adminService, "account-identifier", accountIdentifier, "org-identifier", orgIdentifier, "sdk-base-url", sdkBaseURL, "sdk-events-url", sdkEventsURL, "redis-addr", redisAddress, "redis-db", redisDB, "api-keys", fmt.Sprintf("%v", apiKeys), "target-poll-duration", fmt.Sprintf("%ds", targetPollDuration), "heartbeat-interval", fmt.Sprintf("%ds", heartbeatInterval), "flag-stream-enabled", flagStreamEnabled, "flag-poll-interval", fmt.Sprintf("%dm", flagPollInterval)) + logger.Info("service config", "pprof", pprofEnabled, "debug", debug, "bypass-auth", bypassAuth, "offline", offline, "port", port, "admin-service", adminService, "account-identifier", accountIdentifier, "org-identifier", orgIdentifier, "sdk-base-url", sdkBaseURL, "sdk-events-url", sdkEventsURL, "redis-addr", redisAddress, "redis-db", redisDB, "api-keys", fmt.Sprintf("%v", apiKeys), "target-poll-duration", fmt.Sprintf("%ds", targetPollDuration), "heartbeat-interval", fmt.Sprintf("%ds", heartbeatInterval), "flag-stream-enabled", flagStreamEnabled, "flag-poll-interval", fmt.Sprintf("%dm", flagPollInterval), "config-dir", configDir) adminService, err := services.NewAdminService(logger, adminService, adminServiceToken) if err != nil { @@ -335,10 +340,8 @@ func main() { // Load either local config from files or remote config from ff-server if offline && !generateOfflineConfig { - // TODO - this works in the built image, see if we can have a better way to automatically work running locally too - // change to fs:= ffproxy.DefaultConfig if running locally - fs := os.DirFS("") - config, err := config.NewLocalConfig(fs, ffproxy.DefaultConfigDir) + fs := os.DirFS(configDir) + config, err := config.NewLocalConfig(fs) if err != nil { logger.Error("failed to load config", "err", err) os.Exit(1) diff --git a/config/local_config.go b/config/local_config.go index e4112707..04aeeab3 100644 --- a/config/local_config.go +++ b/config/local_config.go @@ -27,13 +27,13 @@ type LocalConfig struct { // NewLocalConfig creates a new FeatureFlagConfig that loads config from // the passed FileSystem and directory. -func NewLocalConfig(fs fs.FS, dir string) (LocalConfig, error) { +func NewLocalConfig(fs fs.FS) (LocalConfig, error) { o := LocalConfig{ config: make(map[string]config), hasher: hash.NewSha256(), } - if err := o.loadConfig(fs, dir); err != nil { + if err := o.loadConfig(fs); err != nil { return LocalConfig{}, err } return o, nil @@ -41,8 +41,8 @@ func NewLocalConfig(fs fs.FS, dir string) (LocalConfig, error) { // loadConfig reads the directory of the filesystem and walks the file tree // decoding any config files that it finds -func (f LocalConfig) loadConfig(fileSystem fs.FS, dir string) error { - if err := fs.WalkDir(fileSystem, dir, decodeConfigFiles(f.config)); err != nil { +func (f LocalConfig) loadConfig(fileSystem fs.FS) error { + if err := fs.WalkDir(fileSystem, ".", decodeConfigFiles(f.config, fileSystem)); err != nil { return err } return nil @@ -51,7 +51,7 @@ func (f LocalConfig) loadConfig(fileSystem fs.FS, dir string) error { // getParentDirFromPath gets the name of the parent directory for a file in a path func getParentDirFromPath(path string) (string, error) { split := strings.SplitAfter(path, "/") - if len(split) <= 2 { + if len(split) < 2 { return "", errors.New("path needs a length of at least 2 to have a parent") } @@ -61,7 +61,7 @@ func getParentDirFromPath(path string) (string, error) { // decodeConfigFiles returns a WalkDirFunc that gets called on each file in the // config directory. -func decodeConfigFiles(c map[string]config) fs.WalkDirFunc { +func decodeConfigFiles(c map[string]config, fileSystem fs.FS) fs.WalkDirFunc { return func(path string, d fs.DirEntry, err error) error { if err != nil { return err @@ -96,7 +96,7 @@ func decodeConfigFiles(c map[string]config) fs.WalkDirFunc { if i.Name() == "feature_config.json" { config := c[env] - if err := ffproxy.DecodeFile(path, &config.FeatureFlags); err != nil { + if err := ffproxy.DecodeFile(fileSystem, path, &config.FeatureFlags); err != nil { return err } c[env] = config @@ -105,7 +105,7 @@ func decodeConfigFiles(c map[string]config) fs.WalkDirFunc { if i.Name() == "targets.json" { config := c[env] - if err := ffproxy.DecodeFile(path, &config.Targets); err != nil { + if err := ffproxy.DecodeFile(fileSystem, path, &config.Targets); err != nil { return err } c[env] = config @@ -114,7 +114,7 @@ func decodeConfigFiles(c map[string]config) fs.WalkDirFunc { if i.Name() == "segments.json" { config := c[env] - if err := ffproxy.DecodeFile(path, &config.Segments); err != nil { + if err := ffproxy.DecodeFile(fileSystem, path, &config.Segments); err != nil { return err } c[env] = config @@ -123,7 +123,7 @@ func decodeConfigFiles(c map[string]config) fs.WalkDirFunc { if i.Name() == "auth_config.json" { config := c[env] - if err := ffproxy.DecodeFile(path, &config.Auth); err != nil { + if err := ffproxy.DecodeFile(fileSystem, path, &config.Auth); err != nil { return err } c[env] = config diff --git a/config/local_config_test.go b/config/local_config_test.go index 688d6597..f476b058 100644 --- a/config/local_config_test.go +++ b/config/local_config_test.go @@ -211,7 +211,7 @@ func TestLocalConfig(t *testing.T) { domain.NewSegmentKey("1234"): []domain.Segment{flagsTeamSegment}, } - lc, err := NewLocalConfig(testConfig, testDir) + lc, err := NewLocalConfig(testConfig) if err != nil { t.Fatal(err) } @@ -237,7 +237,7 @@ func TestLocalConfig_Auth(t *testing.T) { domain.AuthAPIKey(apikey3Hash): "1234", } - lc, err := NewLocalConfig(testConfig, testDir) + lc, err := NewLocalConfig(testConfig) if err != nil { t.Fatal(err) } diff --git a/config/remote_config_test.go b/config/remote_config_test.go index c1b0d129..b86a9642 100644 --- a/config/remote_config_test.go +++ b/config/remote_config_test.go @@ -431,10 +431,15 @@ func TestPollTargets(t *testing.T) { close(ticker) } + targetsCopy := map[string][]admingen.Target{} + for key, value := range targets{ + targetsCopy[key] = value + } + adminClient := mockAdminClient{ projects: projects, environments: environments, - targets: targets, + targets: targetsCopy, Mutex: &sync.Mutex{}, } @@ -444,7 +449,6 @@ func TestPollTargets(t *testing.T) { } if len(tc.targetsToAdd) > 0 { - t.Log("And I add Targets to the admin client") key := string("FeatureFlagsDev-Dev") adminClient.Lock() diff --git a/embed.go b/embed.go deleted file mode 100644 index 73564900..00000000 --- a/embed.go +++ /dev/null @@ -1,16 +0,0 @@ -package ffproxy - -import "embed" - -var ( - // DefaultConfig embeds the default config directory and the env directories - // that we care about reading configuration from - //go:embed config/env-* - DefaultConfig embed.FS -) - -const ( - // DefaultConfigDir is the name of the default directory where the files for - // side loading FeatureFlagConfig live - DefaultConfigDir = "config" -) diff --git a/file_decoder.go b/file_decoder.go index 4dce037e..0979b759 100644 --- a/file_decoder.go +++ b/file_decoder.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" "io" - "os" + "io/fs" "path/filepath" "gopkg.in/yaml.v2" @@ -18,8 +18,8 @@ const ( ) // DecodeFile is a convienence function that creates a FileDecoder and calls Decode -func DecodeFile(path string, v interface{}) error { - dec, err := NewFileDecoder(path) +func DecodeFile(fileSystem fs.FS, path string, v interface{}) error { + dec, err := NewFileDecoder(fileSystem, path) if err != nil { return err } @@ -42,13 +42,13 @@ type FileDecoder struct { // If the file extension is not supported it returns an error. NewFileDecoder does // not close the opened file, for the file to be closed you have to call the Decode // method. -func NewFileDecoder(file string) (*FileDecoder, error) { - f, err := os.Open(filepath.Clean(file)) +func NewFileDecoder(fileSystem fs.FS, file string) (*FileDecoder, error) { + f, err := fileSystem.Open(file) if err != nil { return nil, err } - ext := filepath.Ext(f.Name()) + ext := filepath.Ext(file) var dec decoder switch ext { diff --git a/proxy-service/service_test.go b/proxy-service/service_test.go index 92f61831..0af23480 100644 --- a/proxy-service/service_test.go +++ b/proxy-service/service_test.go @@ -46,7 +46,7 @@ func getAllBenchmarkConfig() benchmarkConfig { dir := fmt.Sprintf("../config/bench-test") fileSystem := fileSystem{path: dir} - lc, err := config.NewLocalConfig(fileSystem, dir) + lc, err := config.NewLocalConfig(fileSystem) if err != nil { panic(err) } @@ -69,7 +69,7 @@ func getConfigByEnv(envID string, b *testing.B) benchmarkConfig { dir := fmt.Sprintf("../config/bench-test/env-%s", envID) fileSystem := fileSystem{path: dir} - lc, err := config.NewLocalConfig(fileSystem, dir) + lc, err := config.NewLocalConfig(fileSystem) if err != nil { b.Fatalf("failed to load config: %s", err) } diff --git a/transport/http_server_test.go b/transport/http_server_test.go index 7e93f56f..1387df61 100644 --- a/transport/http_server_test.go +++ b/transport/http_server_test.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "io" - "io/fs" "net" "net/http" "net/http/httptest" @@ -70,18 +69,6 @@ func (m *mockMetricService) StoreMetrics(ctx context.Context, req domain.Metrics return m.storeMetrics(ctx, req) } -type fileSystem struct { - path string -} - -func (f fileSystem) Open(name string) (fs.File, error) { - file, err := os.Open(name) - if err != nil { - return nil, err - } - return file, nil -} - const ( apiKey1 = "apikey1" envID123 = "1234" @@ -156,8 +143,8 @@ func setupWithCache(c cache.Cache) setupOpts { // setupHTTPServer is a helper that loads test config for populating the repos // and injects all the required dependencies into the proxy service and http server func setupHTTPServer(t *testing.T, bypassAuth bool, opts ...setupOpts) *HTTPServer { - fileSystem := fileSystem{path: "../config/test"} - config, err := config.NewLocalConfig(fileSystem, "../config/test") + fileSystem := os.DirFS("../config/test") + config, err := config.NewLocalConfig(fileSystem) if err != nil { t.Fatal(err) }