github.com/ystia/yorc/v4@v4.3.0/storage/store_mgr.go (about) 1 // Copyright 2019 Bull S.A.S. Atos Technologies - Bull, Rue Jean Jaures, B.P.68, 78340, Les Clayes-sous-Bois, France. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package storage 16 17 import ( 18 "context" 19 "encoding/json" 20 "fmt" 21 "math/rand" 22 "path" 23 "strings" 24 "time" 25 26 "github.com/matryer/resync" 27 "github.com/pkg/errors" 28 29 "github.com/ystia/yorc/v4/config" 30 "github.com/ystia/yorc/v4/helper/collections" 31 "github.com/ystia/yorc/v4/helper/consulutil" 32 "github.com/ystia/yorc/v4/log" 33 "github.com/ystia/yorc/v4/storage/internal/consul" 34 "github.com/ystia/yorc/v4/storage/internal/elastic" 35 "github.com/ystia/yorc/v4/storage/internal/file" 36 "github.com/ystia/yorc/v4/storage/store" 37 "github.com/ystia/yorc/v4/storage/types" 38 ) 39 40 const consulStoreImpl = "consul" 41 42 const elasticStoreImpl = "elastic" 43 44 const fileStoreImpl = "file" 45 46 const fileStoreWithEncryptionImpl = "cipherFile" 47 48 const fileStoreWithCacheImpl = "fileCache" 49 50 const fileStoreWithCacheAndEncryptionImpl = "cipherFileCache" 51 52 const defaultRelativeRootDir = "store" 53 54 const defaultBlockingQueryTimeout = "5m0s" 55 56 // Default num counters for file cache (100 000) 57 // NumCounters is the number of 4-bit access counters to keep for admission and eviction 58 // We've seen good performance in setting this to 10x the number of items you expect to keep in the cache when full. 59 const defaultCacheNumCounters = "1e5" 60 61 // MaxCost can be used to denote the max size in bytes 62 const defaultCacheMaxCost = "1e7" 63 64 const defaultCacheBufferItems = "64" 65 66 var once resync.Once 67 68 // stores implementations provided with GetStore(types.StoreType) 69 var stores map[types.StoreType]store.Store 70 71 // default config stores loaded at init 72 var defaultConfigStores map[string]config.Store 73 74 func completePropertiesWithDefault(cfg config.Configuration, props config.DynamicMap) config.DynamicMap { 75 complete := config.DynamicMap{} 76 77 for k, v := range props { 78 complete[k] = v 79 } 80 // Complete properties with string values as it's stored in Consul KV 81 if !complete.IsSet("root_dir") { 82 complete["root_dir"] = path.Join(cfg.WorkingDirectory, defaultRelativeRootDir) 83 } 84 if !complete.IsSet("blocking_query_default_timeout") { 85 complete["blocking_query_default_timeout"] = defaultBlockingQueryTimeout 86 } 87 if !complete.IsSet("cache_num_counters") { 88 complete["cache_num_counters"] = defaultCacheNumCounters 89 } 90 if !complete.IsSet("cache_max_cost") { 91 complete["cache_max_cost"] = defaultCacheMaxCost 92 } 93 if !complete.IsSet("cache_buffer_items") { 94 complete["cache_buffer_items"] = defaultCacheBufferItems 95 } 96 return complete 97 } 98 99 func initDefaultConfigStores(cfg config.Configuration) { 100 defaultConfigStores = make(map[string]config.Store, 0) 101 props := completePropertiesWithDefault(cfg, cfg.Storage.DefaultProperties) 102 // File with cache store for deployments 103 fileStoreWithCache := config.Store{ 104 Name: "defaultFileStoreWithCache", 105 Implementation: fileStoreWithCacheImpl, 106 Types: []string{types.StoreTypeDeployment.String()}, 107 Properties: props, 108 } 109 defaultConfigStores[fileStoreWithCache.Name] = fileStoreWithCache 110 111 // Consul store for both logs and events 112 consulStore := config.Store{ 113 Name: "defaultConsulStore", 114 Implementation: consulStoreImpl, 115 Types: []string{types.StoreTypeLog.String(), types.StoreTypeEvent.String()}, 116 } 117 defaultConfigStores[consulStore.Name] = consulStore 118 119 // Consul store for logs only 120 consulStoreLog := config.Store{ 121 Name: "defaultConsulStore" + types.StoreTypeLog.String(), 122 Implementation: consulStoreImpl, 123 Types: []string{types.StoreTypeLog.String()}, 124 } 125 defaultConfigStores[consulStoreLog.Name] = consulStore 126 127 // Consul store for events only 128 consulStoreEvent := config.Store{ 129 Name: "defaultConsulStore" + types.StoreTypeEvent.String(), 130 Implementation: consulStoreImpl, 131 Types: []string{types.StoreTypeEvent.String()}, 132 } 133 defaultConfigStores[consulStoreEvent.Name] = consulStoreEvent 134 } 135 136 // LoadStores reads/saves stores configuration and load store implementations in mem. 137 // The store config needs to provide store for all defined types. ie. deployments, logs and events. 138 // The stores config is saved once and can be reset if storage.reset is true. 139 func LoadStores(cfg config.Configuration) error { 140 //time.Sleep(10 * time.Second) 141 var err error 142 // load stores once 143 once.Do(func() { 144 var cfgStores []config.Store 145 var init bool 146 // load stores config from Consul if already present or save them from configuration 147 init, cfgStores, err = getConfigStores(cfg) 148 if err != nil { 149 return 150 } 151 152 // load stores implementations 153 stores = make(map[types.StoreType]store.Store, 0) 154 for _, configStore := range cfgStores { 155 var storeImpl store.Store 156 storeImpl, err = createStoreImpl(cfg, configStore) 157 if err != nil { 158 return 159 } 160 for _, storeTypeName := range configStore.Types { 161 st, _ := types.ParseStoreType(storeTypeName) 162 if _, ok := stores[st]; !ok { 163 log.Printf("Using store with name:%q, implementation:%q for type: %q", configStore.Name, configStore.Implementation, storeTypeName) 164 stores[st] = storeImpl 165 166 // Handle Consul data migration for log/event stores 167 if configStore.MigrateDataFromConsul && init && configStore.Implementation != consulStoreImpl { 168 err = migrateData(configStore.Name, st, stores[st]) 169 if err != nil { 170 return 171 } 172 } 173 } 174 } 175 } 176 }) 177 178 if err != nil { 179 clearConfigStore() 180 } 181 return err 182 } 183 184 func getConfigStores(cfg config.Configuration) (bool, []config.Store, error) { 185 consulClient, err := cfg.GetConsulClient() 186 if err != nil { 187 return false, nil, err 188 } 189 lock, err := consulutil.AcquireLock(consulClient, ".lock_stores", 0) 190 if err != nil { 191 return false, nil, err 192 } 193 defer lock.Unlock() 194 195 kvps, _, err := consulClient.KV().List(consulutil.StoresPrefix, nil) 196 if err != nil { 197 return false, nil, errors.Wrap(err, consulutil.ConsulGenericErrMsg) 198 } 199 200 // Get config Store from Consul if reset is false and exists any store 201 if !cfg.Storage.Reset && len(kvps) > 0 { 202 log.Debugf("Found %d stores already saved", len(kvps)) 203 configStores := make([]config.Store, len(kvps)) 204 for _, kvp := range kvps { 205 name := path.Base(kvp.Key) 206 configStore := new(config.Store) 207 err = json.Unmarshal(kvp.Value, configStore) 208 if err != nil { 209 return false, nil, errors.Wrapf(err, "failed to unmarshal store with name:%q", name) 210 } 211 configStores = append(configStores, *configStore) 212 } 213 return false, configStores, nil 214 } 215 configStores, err := initConfigStores(cfg) 216 return true, configStores, err 217 } 218 219 // Initialize config stores in Consul 220 func initConfigStores(cfg config.Configuration) ([]config.Store, error) { 221 cfgStores, err := checkAndBuildConfigStores(cfg) 222 if err != nil { 223 return nil, err 224 } 225 226 if err := clearConfigStore(); err != nil { 227 return nil, err 228 } 229 // Save stores config in Consul 230 for _, configStore := range cfgStores { 231 err := consulutil.StoreConsulKeyWithJSONValue(path.Join(consulutil.StoresPrefix, configStore.Name), configStore) 232 if err != nil { 233 return nil, errors.Wrapf(err, "failed to save store %s in consul", configStore.Name) 234 } 235 log.Debugf("Save store config with name:%q", configStore.Name) 236 } 237 return cfgStores, nil 238 } 239 240 // Clear config stores in Consul 241 func clearConfigStore() error { 242 return consulutil.Delete(consulutil.StoresPrefix, true) 243 } 244 245 func getDefaultConfigStores(cfg config.Configuration) []config.Store { 246 if defaultConfigStores == nil { 247 initDefaultConfigStores(cfg) 248 } 249 250 return []config.Store{ 251 defaultConfigStores["defaultFileStoreWithCache"], 252 defaultConfigStores["defaultConsulStore"], 253 } 254 } 255 256 // Build default config stores for missing one of specified type 257 func getDefaultConfigStore(cfg config.Configuration, storeTypeName string) config.Store { 258 if defaultConfigStores == nil { 259 initDefaultConfigStores(cfg) 260 } 261 262 var defaultStore config.Store 263 switch storeTypeName { 264 case types.StoreTypeDeployment.String(): 265 defaultStore = defaultConfigStores["defaultFileStoreWithCache"] 266 case types.StoreTypeEvent.String(): 267 defaultStore = defaultConfigStores["defaultConsulStore"+types.StoreTypeEvent.String()] 268 case types.StoreTypeLog.String(): 269 defaultStore = defaultConfigStores["defaultFileStoreWithCache"+types.StoreTypeLog.String()] 270 } 271 return defaultStore 272 } 273 274 func storeExists(stores []config.Store, name string) bool { 275 for _, storeItem := range stores { 276 if storeItem.Name == name { 277 return true 278 } 279 } 280 return false 281 } 282 283 // Check if all stores types are provided by stores config 284 // If no config is provided, global default config store is added 285 // If any store type is missing, a related default config store is added 286 func checkAndBuildConfigStores(cfg config.Configuration) ([]config.Store, error) { 287 if cfg.Storage.Stores == nil { 288 return getDefaultConfigStores(cfg), nil 289 } 290 291 cfgStores := make([]config.Store, 0) 292 checkStoreTypes := make([]string, 0) 293 checkStoreNames := make([]string, 0) 294 for _, cfgStore := range cfg.Storage.Stores { 295 if cfgStore.Implementation == "" { 296 return nil, errors.Errorf("Missing mandatory property \"implementation\" for store with name:%q", cfgStore.Name) 297 } 298 if cfgStore.Types == nil || len(cfgStore.Types) == 0 { 299 return nil, errors.Errorf("Missing mandatory property \"types\" for store with name:%q", cfgStore.Name) 300 } 301 302 if cfgStore.Name == "" { 303 rand.Seed(time.Now().UnixNano()) 304 extra := rand.Intn(100) 305 cfgStore.Name = fmt.Sprintf("%s%s-%d", cfgStore.Implementation, strings.Join(cfgStore.Types, ""), extra) 306 } 307 308 // Check store name is unique 309 if collections.ContainsString(checkStoreNames, cfgStore.Name) { 310 return nil, errors.Errorf("At least, 2 different stores have the same name:%q", cfgStore.Name) 311 } 312 313 // Complete store properties with default for fileCache implementation 314 if cfgStore.Implementation == fileStoreWithCacheAndEncryptionImpl || cfgStore.Implementation == fileStoreWithCacheImpl || cfgStore.Implementation == fileStoreImpl { 315 props := completePropertiesWithDefault(cfg, cfgStore.Properties) 316 cfgStore.Properties = props 317 } 318 319 checkStoreNames = append(checkStoreNames, cfgStore.Name) 320 321 // Prepare store types check 322 // First store type occurrence is taken in account 323 for _, storeTypeName := range cfgStore.Types { 324 // let's do this case insensitive 325 name := strings.ToLower(storeTypeName) 326 if !collections.ContainsString(checkStoreTypes, name) { 327 checkStoreTypes = append(checkStoreTypes, name) 328 // Add the store for this store type if not already added 329 if !storeExists(cfgStores, cfgStore.Name) { 330 log.Printf("Provided config store will be used for store type:%q.", storeTypeName) 331 cfgStores = append(cfgStores, cfgStore) 332 } 333 } 334 } 335 } 336 337 // Check each store type has its implementation. 338 // Add default if none is provided by config 339 for _, storeTypeName := range types.StoreTypeNames() { 340 name := strings.ToLower(storeTypeName) 341 if !collections.ContainsString(checkStoreTypes, name) { 342 log.Printf("Default config store will be used for store type:%q.", storeTypeName) 343 cfgStores = append(cfgStores, getDefaultConfigStore(cfg, storeTypeName)) 344 } 345 } 346 return cfgStores, nil 347 } 348 349 // Create store implementations 350 func createStoreImpl(cfg config.Configuration, configStore config.Store) (store.Store, error) { 351 var storeImpl store.Store 352 var err error 353 impl := strings.ToLower(configStore.Implementation) 354 switch impl { 355 case strings.ToLower(fileStoreWithCacheImpl), strings.ToLower(fileStoreWithCacheAndEncryptionImpl): 356 storeImpl, err = file.NewStore(cfg, configStore.Name, configStore.Properties, true, impl == strings.ToLower(fileStoreWithCacheAndEncryptionImpl)) 357 if err != nil { 358 return nil, err 359 } 360 case strings.ToLower(fileStoreImpl), strings.ToLower(fileStoreWithEncryptionImpl): 361 storeImpl, err = file.NewStore(cfg, configStore.Name, configStore.Properties, false, impl == strings.ToLower(fileStoreWithEncryptionImpl)) 362 if err != nil { 363 return nil, err 364 } 365 case strings.ToLower(consulStoreImpl): 366 storeImpl = consul.NewStore() 367 case strings.ToLower(elasticStoreImpl): 368 storeImpl, err = elastic.NewStore(cfg, configStore) 369 if err != nil { 370 return nil, err 371 } 372 default: 373 log.Printf("[WARNING] unknown store implementation:%q. This will be ignored.", impl) 374 } 375 return storeImpl, nil 376 } 377 378 // this allows to migrate log or events from Consul to new store implementations (other than Consul) 379 func migrateData(storeName string, storeType types.StoreType, storeImpl store.Store) error { 380 381 var rootPath string 382 switch storeType { 383 case types.StoreTypeLog: 384 rootPath = consulutil.LogsPrefix 385 case types.StoreTypeEvent: 386 rootPath = consulutil.EventsPrefix 387 default: 388 log.Printf("[WARNING] No migration handled for type:%q (demanded in config for store name:%q)", storeType, storeName) 389 return nil 390 } 391 kvps, _, err := consulutil.GetKV().List(rootPath, nil) 392 if err != nil { 393 return errors.Wrapf(err, "failed to migrate data from Consul for root path:%q in store with name:%q", rootPath, storeName) 394 } 395 if kvps == nil || len(kvps) == 0 { 396 return nil 397 } 398 keyValues := make([]store.KeyValueIn, 0) 399 var value json.RawMessage 400 for _, kvp := range kvps { 401 value = kvp.Value 402 keyValues = append(keyValues, store.KeyValueIn{ 403 Key: kvp.Key, 404 Value: value, 405 }) 406 407 } 408 err = storeImpl.SetCollection(context.Background(), keyValues) 409 if err != nil { 410 return errors.Wrapf(err, "failed to migrate data from Consul for root path:%q in store with name:%q", rootPath, storeName) 411 } 412 413 return consulutil.Delete(rootPath, true) 414 } 415 416 // GetStore returns the store related to a defined store type 417 func GetStore(tType types.StoreType) store.Store { 418 store, ok := stores[tType] 419 if !ok { 420 log.Panic("Store %q is missing. This is not expected.", tType.String()) 421 } 422 return store 423 }