go.ligato.io/vpp-agent/v3@v3.5.0/plugins/orchestrator/localregistry/initfileregistry.go (about)

     1  // Copyright (c) 2020 Pantheon.tech
     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 localregistry
    16  
    17  import (
    18  	"fmt"
    19  	"os"
    20  	"path/filepath"
    21  
    22  	yaml2 "github.com/ghodss/yaml"
    23  	"go.ligato.io/cn-infra/v2/config"
    24  	"go.ligato.io/cn-infra/v2/datasync"
    25  	"go.ligato.io/cn-infra/v2/datasync/kvdbsync/local"
    26  	"go.ligato.io/cn-infra/v2/datasync/resync"
    27  	"go.ligato.io/cn-infra/v2/datasync/syncbase"
    28  	"go.ligato.io/cn-infra/v2/db/keyval"
    29  	"go.ligato.io/cn-infra/v2/infra"
    30  	"google.golang.org/protobuf/encoding/protojson"
    31  	"google.golang.org/protobuf/proto"
    32  
    33  	"go.ligato.io/vpp-agent/v3/client"
    34  	"go.ligato.io/vpp-agent/v3/plugins/orchestrator"
    35  )
    36  
    37  const (
    38  	registryName        = "init-file-registry"
    39  	defaultInitFilePath = "initial-config.yaml"
    40  )
    41  
    42  type Option func(*InitFileRegistry)
    43  
    44  // InitFileRegistry is local read-only NB configuration provider with exclusive data source from a file
    45  // given by a file path (InitConfigFilePath). Its purpose is to seamlessly integrated NB configuration
    46  // from file as another NB configuration provider (to existing providers: etcd, consul, redis) and integrate
    47  // it's configuration into agent in the same standard way(datasync.KeyValProtoWatcher). The content of this
    48  // registry is meant to be only the initial NB configuration for the agent and will not reflect any changes
    49  // inside given file after initial content loading.
    50  //
    51  // The NB configuration provisioning process and how this registry fits into it:
    52  //  1. NB data sources register to default resync plugin (InitFileRegistry registers too in watchNBResync(),
    53  //     but only when there are some NB config data from file, otherwise it makes no sense to register because
    54  //     there is nothing to forward. This also means that before register to resync plugin, the NB config from
    55  //     file will be preloaded)
    56  //  2. Call to resync plugin's DoResync triggers resync to NB configuration sources (InitFileRegistry takes
    57  //     its preloaded NB config and stores it into another inner local registry)
    58  //  3. NB configuration sources are also watchable (datasync.KeyValProtoWatcher) and the resync data is
    59  //     collected by the watcher.Aggregator (InitFileRegistry is also watchable/forwards data to watcher.Aggregator,
    60  //     it relies on the watcher capabilities of its inner local registry. This is the cause why to preloaded
    61  //     the NB config from file([]proto.Message storage) and push it to another inner local storage later
    62  //     (syncbase.Registry). If we used only one storage (syncbase.Registry for its watch capabilities), we
    63  //     couldn't answer some questins about the storage soon enough (watcher.Aggregator in Watch(...) needs to
    64  //     know whether this storage will send some data or not, otherwise the retrieval can hang on waiting for
    65  //     data that never come))
    66  //  4. watcher.Aggregator merges all collected resync data and forwards them its watch clients (it also implements
    67  //     datasync.KeyValProtoWatcher just like the NB data sources).
    68  //  5. Clients of Aggregator (currently orchestrator and ifplugin) handle the NB changes/resync properly.
    69  type InitFileRegistry struct {
    70  	infra.PluginDeps
    71  
    72  	initialized             bool
    73  	config                  *Config
    74  	watchedRegistry         *syncbase.Registry
    75  	pushedToWatchedRegistry bool
    76  	preloadedNBConfigs      []proto.Message
    77  }
    78  
    79  // Config holds the InitFileRegistry configuration.
    80  type Config struct {
    81  	DisableInitialConfiguration  bool   `json:"disable-initial-configuration"`
    82  	InitialConfigurationFilePath string `json:"initial-configuration-file-path"`
    83  }
    84  
    85  // NewInitFileRegistryPlugin creates a new InitFileRegistry Plugin with the provided Options
    86  func NewInitFileRegistryPlugin(opts ...Option) *InitFileRegistry {
    87  	p := &InitFileRegistry{}
    88  
    89  	p.PluginName = "initfileregistry"
    90  	p.watchedRegistry = syncbase.NewRegistry()
    91  
    92  	for _, o := range opts {
    93  		o(p)
    94  	}
    95  	if p.Cfg == nil {
    96  		p.Cfg = config.ForPlugin(p.String(),
    97  			config.WithCustomizedFlag(config.FlagName(p.String()), "initfileregistry.conf"),
    98  		)
    99  	}
   100  	p.PluginDeps.SetupLog()
   101  
   102  	return p
   103  }
   104  
   105  // Init initialize registry
   106  func (r *InitFileRegistry) Init() error {
   107  	if !r.initialized {
   108  		return r.initialize()
   109  	}
   110  	return nil
   111  }
   112  
   113  // Empty checks whether this registry holds data or not. As result of the properties of this registry
   114  // (readonly, will be filled only once from initial file import), this method directly indicates whether
   115  // the watchers of this registry will receive any data (Empty() == false, receive initial resync) or
   116  // won't receive anything at all (Empty() == true)
   117  func (r *InitFileRegistry) Empty() bool {
   118  	if !r.initialized { // could be called from init of other plugins -> possibly before this plugin init
   119  		if err := r.initialize(); err != nil {
   120  			r.Log.Errorf("cannot initialize InitFileRegistry due to: %v", err)
   121  		}
   122  	}
   123  	return len(r.preloadedNBConfigs) == 0
   124  }
   125  
   126  // Watch functionality is forwarded to inner syncbase.Registry. For some watchers might be relevant
   127  // whether any data will be pushed to them at all (i.e. watcher.Aggregator). They should use the
   128  // Empty() method to find out whether there are (=ever will be do to nature of this registry) any
   129  // data for pushing to watchers.
   130  func (r *InitFileRegistry) Watch(resyncName string, changeChan chan datasync.ChangeEvent,
   131  	resyncChan chan datasync.ResyncEvent, keyPrefixes ...string) (datasync.WatchRegistration, error) {
   132  	return r.watchedRegistry.Watch(resyncName, changeChan, resyncChan, keyPrefixes...)
   133  }
   134  
   135  // initialize will try to pre-load the NB initial data
   136  // (watchers of this registry will receive it only after call to resync)
   137  func (r *InitFileRegistry) initialize() error {
   138  	defer func() {
   139  		r.initialized = true
   140  	}()
   141  
   142  	// parse configuration file
   143  	var err error
   144  	r.config, err = r.retrieveConfig()
   145  	if err != nil {
   146  		return err
   147  	}
   148  
   149  	// Initial NB configuration loaded from file
   150  	if !r.config.DisableInitialConfiguration {
   151  		// preload NB config data from file
   152  		if err := r.preloadNBConfigs(r.config.InitialConfigurationFilePath); err != nil {
   153  			return fmt.Errorf("cannot preload initial NB configuration from file due to: %w", err)
   154  		}
   155  		if len(r.preloadedNBConfigs) != 0 {
   156  			// watch for resync.DefaultPlugin.DoResync() that will trigger pushing of preloaded
   157  			// NB config data from file into NB aggregator watcher
   158  			// (see InitFileRegistry struct docs for detailed explanation)
   159  			r.watchNBResync()
   160  		}
   161  	}
   162  	return nil
   163  }
   164  
   165  // retrieveConfig loads InitFileRegistry plugin configuration file.
   166  func (r *InitFileRegistry) retrieveConfig() (*Config, error) {
   167  	cfg := &Config{
   168  		// default configuration
   169  		DisableInitialConfiguration:  false,
   170  		InitialConfigurationFilePath: defaultInitFilePath,
   171  	}
   172  	found, err := r.Cfg.LoadValue(cfg)
   173  	if !found {
   174  		if err == nil {
   175  			r.Log.Debug("InitFileRegistry plugin config not found")
   176  		} else {
   177  			r.Log.Debugf("InitFileRegistry plugin config cannot be loaded due to: %v", err)
   178  		}
   179  		return cfg, err
   180  	}
   181  	if err != nil {
   182  		return nil, err
   183  	}
   184  	return cfg, err
   185  }
   186  
   187  // watchNBResync will watch to default resync plugin's resync call(resync.DefaultPlugin.DoResync()) and will
   188  // load NB initial config from file (already preloaded from initialize()) when the first resync will be fired.
   189  func (r *InitFileRegistry) watchNBResync() {
   190  	registration := resync.DefaultPlugin.Register(registryName)
   191  	go r.watchResync(registration)
   192  }
   193  
   194  // watchResync will listen to resync plugin resync signals and at first resync will push the preloaded
   195  // NB initial config into internal local register (p.registry)
   196  func (r *InitFileRegistry) watchResync(resyncReg resync.Registration) {
   197  	for resyncStatus := range resyncReg.StatusChan() {
   198  		// resyncReg.StatusChan == Started => resync
   199  		if resyncStatus.ResyncStatus() == resync.Started && !r.pushedToWatchedRegistry {
   200  			if !r.Empty() { // put preloaded NB init file data into watched p.registry
   201  				c := client.NewClient(&txnFactory{r.watchedRegistry}, &orchestrator.DefaultPlugin)
   202  				if err := c.ResyncConfig(r.preloadedNBConfigs...); err != nil {
   203  					r.Log.Errorf("resyncing preloaded NB init file data "+
   204  						"into watched registry failed: %w", err)
   205  				}
   206  			}
   207  			r.pushedToWatchedRegistry = true
   208  			resyncStatus.Ack()
   209  			// TODO some done channel to not continue as NOP goroutine
   210  			continue // can't unregister anymore -> need to listen to further resync signals, but it will be just NO-OPs
   211  		}
   212  		resyncStatus.Ack()
   213  	}
   214  }
   215  
   216  // preloadNBConfigs imports NB configuration from file(filepath) into preloadedNBConfigs. If file is not found,
   217  // it is not considered as error, but as a sign that the NB-configuration-loading-from-file feature should be
   218  // not used (inner registry remains empty and watchers of this registry get no data).
   219  func (r *InitFileRegistry) preloadNBConfigs(filePath string) error {
   220  	// check existence of NB init file
   221  	if _, err := os.Stat(filePath); os.IsNotExist(err) {
   222  		filePath := filePath
   223  		if absFilePath, err := filepath.Abs(filePath); err == nil {
   224  			filePath = absFilePath
   225  		}
   226  		r.Log.Debugf("Initialization configuration file(%v) not found. "+
   227  			"Skipping its preloading.", filePath)
   228  		return nil
   229  	}
   230  
   231  	// read data from file
   232  	b, err := os.ReadFile(filePath)
   233  	if err != nil {
   234  		return fmt.Errorf("problem reading file %s: %w", filePath, err)
   235  	}
   236  
   237  	// create dynamic config (using it instead of configurator.Config because it can hold also models defined
   238  	// outside the VPP-Agent repo, i.e. if this code is using 3rd party code based on VPP-Agent and having its
   239  	// additional properly registered configuration models)
   240  	knownModels, err := client.LocalClient.KnownModels("config") // locally registered models
   241  	if err != nil {
   242  		return fmt.Errorf("cannot get registered models: %w", err)
   243  	}
   244  	cfg, err := client.NewDynamicConfig(knownModels)
   245  	if err != nil {
   246  		return fmt.Errorf("cannot create dynamic config due to: %w", err)
   247  	}
   248  
   249  	// filling dynamically created config with data from NB init file
   250  	bj, err := yaml2.YAMLToJSON(b)
   251  	if err != nil {
   252  		return fmt.Errorf("cannot converting to JSON: %w", err)
   253  	}
   254  	err = protojson.Unmarshal(bj, cfg)
   255  	if err != nil {
   256  		return fmt.Errorf("cannot unmarshall init file data into dynamic config due to: %w", err)
   257  	}
   258  
   259  	// extracting proto messages from dynamic config structure
   260  	// (generic client wants single proto messages and not one big hierarchical config)
   261  	configMessages, err := client.DynamicConfigExport(cfg)
   262  	if err != nil {
   263  		return fmt.Errorf("cannot extract single init configuration proto messages "+
   264  			"from one big configuration proto message due to: %w", err)
   265  	}
   266  
   267  	// remember extracted data for later push to watched registry
   268  	r.preloadedNBConfigs = configMessages
   269  
   270  	return nil
   271  }
   272  
   273  type txnFactory struct {
   274  	registry *syncbase.Registry
   275  }
   276  
   277  func (p *txnFactory) NewTxn(resync bool) keyval.ProtoTxn {
   278  	if resync {
   279  		return local.NewProtoTxn(p.registry.PropagateResync)
   280  	}
   281  	return local.NewProtoTxn(p.registry.PropagateChanges)
   282  }