...

Source file src/github.com/launchdarkly/go-server-sdk/v6/ldfiledata/file_data_source_impl.go

Documentation: github.com/launchdarkly/go-server-sdk/v6/ldfiledata

     1  package ldfiledata
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  	"sync"
    10  	"time"
    11  	"unicode"
    12  
    13  	"github.com/launchdarkly/go-sdk-common/v3/ldlog"
    14  	"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
    15  	"github.com/launchdarkly/go-server-sdk-evaluation/v2/ldbuilders"
    16  	"github.com/launchdarkly/go-server-sdk-evaluation/v2/ldmodel"
    17  	"github.com/launchdarkly/go-server-sdk/v6/interfaces"
    18  	"github.com/launchdarkly/go-server-sdk/v6/internal/datakinds"
    19  	"github.com/launchdarkly/go-server-sdk/v6/subsystems"
    20  	"github.com/launchdarkly/go-server-sdk/v6/subsystems/ldstoretypes"
    21  
    22  	"gopkg.in/ghodss/yaml.v1"
    23  )
    24  
    25  type fileDataSource struct {
    26  	dataSourceUpdates     subsystems.DataSourceUpdateSink
    27  	absFilePaths          []string
    28  	duplicateKeysHandling DuplicateKeysHandling
    29  	reloaderFactory       ReloaderFactory
    30  	loggers               ldlog.Loggers
    31  	isInitialized         bool
    32  	readyCh               chan<- struct{}
    33  	readyOnce             sync.Once
    34  	closeOnce             sync.Once
    35  	closeReloaderCh       chan struct{}
    36  }
    37  
    38  func newFileDataSourceImpl(
    39  	context subsystems.ClientContext,
    40  	dataSourceUpdates subsystems.DataSourceUpdateSink,
    41  	filePaths []string,
    42  	duplicateKeysHandling DuplicateKeysHandling,
    43  	reloaderFactory ReloaderFactory,
    44  ) (subsystems.DataSource, error) {
    45  	abs, err := absFilePaths(filePaths)
    46  	if err != nil {
    47  		// COVERAGE: there's no reliable cross-platform way to simulate an invalid path in unit tests
    48  		return nil, err
    49  	}
    50  
    51  	fs := &fileDataSource{
    52  		dataSourceUpdates:     dataSourceUpdates,
    53  		absFilePaths:          abs,
    54  		duplicateKeysHandling: duplicateKeysHandling,
    55  		reloaderFactory:       reloaderFactory,
    56  		loggers:               context.GetLogging().Loggers,
    57  	}
    58  	fs.loggers.SetPrefix("FileDataSource:")
    59  	return fs, nil
    60  }
    61  
    62  func (fs *fileDataSource) IsInitialized() bool {
    63  	return fs.isInitialized
    64  }
    65  
    66  func (fs *fileDataSource) Start(closeWhenReady chan<- struct{}) {
    67  	fs.readyCh = closeWhenReady
    68  	fs.reload()
    69  
    70  	// If there is no reloader, then we signal readiness immediately regardless of whether the
    71  	// data load succeeded or failed.
    72  	if fs.reloaderFactory == nil {
    73  		fs.signalStartComplete(fs.isInitialized)
    74  		return
    75  	}
    76  
    77  	// If there is a reloader, and if we haven't yet successfully loaded data, then the
    78  	// readiness signal will happen the first time we do get valid data (in reload).
    79  	fs.closeReloaderCh = make(chan struct{})
    80  	err := fs.reloaderFactory(fs.absFilePaths, fs.loggers, fs.reload, fs.closeReloaderCh)
    81  	if err != nil {
    82  		fs.loggers.Errorf("Unable to start reloader: %s\n", err)
    83  	}
    84  }
    85  
    86  // Reload tells the data source to immediately attempt to reread all of the configured source files
    87  // and update the feature flag state. If any file cannot be loaded or parsed, the flag state will not
    88  // be modified.
    89  func (fs *fileDataSource) reload() {
    90  	if fs.closeReloaderCh != nil {
    91  		fs.loggers.Info("Reloading flag data after detecting a change")
    92  	}
    93  	filesData := make([]fileData, 0)
    94  	for _, path := range fs.absFilePaths {
    95  		data, err := readFile(path)
    96  		if err == nil {
    97  			filesData = append(filesData, data)
    98  		} else {
    99  			fs.loggers.Errorf("Unable to load flags: %s [%s]", err, path)
   100  			fs.dataSourceUpdates.UpdateStatus(interfaces.DataSourceStateInterrupted,
   101  				interfaces.DataSourceErrorInfo{
   102  					Kind:    interfaces.DataSourceErrorKindInvalidData,
   103  					Message: err.Error(),
   104  					Time:    time.Now(),
   105  				})
   106  			return
   107  		}
   108  	}
   109  	storeData, err := mergeFileData(fs.duplicateKeysHandling, filesData...)
   110  	if err == nil {
   111  		if fs.dataSourceUpdates.Init(storeData) {
   112  			fs.signalStartComplete(true)
   113  			fs.dataSourceUpdates.UpdateStatus(interfaces.DataSourceStateValid, interfaces.DataSourceErrorInfo{})
   114  		}
   115  	} else {
   116  		fs.dataSourceUpdates.UpdateStatus(interfaces.DataSourceStateInterrupted,
   117  			interfaces.DataSourceErrorInfo{
   118  				Kind:    interfaces.DataSourceErrorKindInvalidData,
   119  				Message: err.Error(),
   120  				Time:    time.Now(),
   121  			})
   122  	}
   123  	if err != nil {
   124  		fs.loggers.Error(err)
   125  	}
   126  }
   127  
   128  func (fs *fileDataSource) signalStartComplete(succeeded bool) {
   129  	fs.readyOnce.Do(func() {
   130  		fs.isInitialized = succeeded
   131  		if fs.readyCh != nil {
   132  			close(fs.readyCh)
   133  		}
   134  	})
   135  }
   136  
   137  func absFilePaths(paths []string) ([]string, error) {
   138  	absPaths := make([]string, 0)
   139  	for _, p := range paths {
   140  		absPath, err := filepath.Abs(p)
   141  		if err != nil {
   142  			// COVERAGE: there's no reliable cross-platform way to simulate an invalid path in unit tests
   143  			return nil, fmt.Errorf("unable to determine absolute path for '%s'", p)
   144  		}
   145  		absPaths = append(absPaths, absPath)
   146  	}
   147  	return absPaths, nil
   148  }
   149  
   150  type fileData struct {
   151  	Flags      *map[string]ldmodel.FeatureFlag
   152  	FlagValues *map[string]ldvalue.Value
   153  	Segments   *map[string]ldmodel.Segment
   154  }
   155  
   156  func insertData(
   157  	all map[ldstoretypes.DataKind]map[string]ldstoretypes.ItemDescriptor,
   158  	kind ldstoretypes.DataKind,
   159  	key string,
   160  	data ldstoretypes.ItemDescriptor,
   161  	duplicateKeysHandling DuplicateKeysHandling,
   162  ) error {
   163  	if _, exists := all[kind][key]; exists {
   164  		switch duplicateKeysHandling {
   165  		case DuplicateKeysIgnoreAllButFirst:
   166  			return nil
   167  		default:
   168  			return fmt.Errorf("%s '%s' is specified by multiple files", kind, key)
   169  		}
   170  	}
   171  	all[kind][key] = data
   172  	return nil
   173  }
   174  
   175  func readFile(path string) (fileData, error) {
   176  	var data fileData
   177  	var rawData []byte
   178  	var err error
   179  	if rawData, err = os.ReadFile(path); err != nil { //nolint:gosec // G304: ok to read file into variable
   180  		return data, fmt.Errorf("unable to read file: %s", err)
   181  	}
   182  	if detectJSON(rawData) {
   183  		err = json.Unmarshal(rawData, &data)
   184  	} else {
   185  		err = yaml.Unmarshal(rawData, &data)
   186  	}
   187  	if err != nil {
   188  		err = fmt.Errorf("error parsing file: %s", err)
   189  	}
   190  	return data, err
   191  }
   192  
   193  func detectJSON(rawData []byte) bool {
   194  	// A valid JSON file for our purposes must be an object, i.e. it must start with '{'
   195  	return strings.HasPrefix(strings.TrimLeftFunc(string(rawData), unicode.IsSpace), "{")
   196  }
   197  
   198  func mergeFileData(
   199  	duplicateKeysHandling DuplicateKeysHandling,
   200  	allFileData ...fileData,
   201  ) ([]ldstoretypes.Collection, error) {
   202  	all := map[ldstoretypes.DataKind]map[string]ldstoretypes.ItemDescriptor{
   203  		datakinds.Features: {},
   204  		datakinds.Segments: {},
   205  	}
   206  	for _, d := range allFileData {
   207  		if d.Flags != nil {
   208  			for key, f := range *d.Flags {
   209  				ff := f
   210  				data := ldstoretypes.ItemDescriptor{Version: f.Version, Item: &ff}
   211  				if err := insertData(all, datakinds.Features, key, data, duplicateKeysHandling); err != nil {
   212  					return nil, err
   213  				}
   214  			}
   215  		}
   216  		if d.FlagValues != nil {
   217  			for key, value := range *d.FlagValues {
   218  				flag := makeFlagWithValue(key, value)
   219  				data := ldstoretypes.ItemDescriptor{Version: flag.Version, Item: flag}
   220  				if err := insertData(all, datakinds.Features, key, data, duplicateKeysHandling); err != nil {
   221  					return nil, err
   222  				}
   223  			}
   224  		}
   225  		if d.Segments != nil {
   226  			for key, s := range *d.Segments {
   227  				ss := s
   228  				data := ldstoretypes.ItemDescriptor{Version: s.Version, Item: &ss}
   229  				if err := insertData(all, datakinds.Segments, key, data, duplicateKeysHandling); err != nil {
   230  					return nil, err
   231  				}
   232  			}
   233  		}
   234  	}
   235  	ret := []ldstoretypes.Collection{}
   236  	for kind, itemsMap := range all {
   237  		items := make([]ldstoretypes.KeyedItemDescriptor, 0, len(itemsMap))
   238  		for k, v := range itemsMap {
   239  			items = append(items, ldstoretypes.KeyedItemDescriptor{Key: k, Item: v})
   240  		}
   241  		ret = append(ret, ldstoretypes.Collection{Kind: kind, Items: items})
   242  	}
   243  	return ret, nil
   244  }
   245  
   246  func makeFlagWithValue(key string, v interface{}) *ldmodel.FeatureFlag {
   247  	flag := ldbuilders.NewFlagBuilder(key).SingleVariation(ldvalue.CopyArbitraryValue(v)).Build()
   248  	return &flag
   249  }
   250  
   251  // Close is called automatically when the client is closed.
   252  func (fs *fileDataSource) Close() (err error) {
   253  	fs.closeOnce.Do(func() {
   254  		if fs.closeReloaderCh != nil {
   255  			close(fs.closeReloaderCh)
   256  		}
   257  	})
   258  	return nil
   259  }
   260  

View as plain text