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
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
71
72 if fs.reloaderFactory == nil {
73 fs.signalStartComplete(fs.isInitialized)
74 return
75 }
76
77
78
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
87
88
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
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 {
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
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
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