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 }