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  }