github.com/quay/claircore@v1.5.28/libvuln/updates/manager.go (about) 1 package updates 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "net/http" 9 "runtime" 10 "strings" 11 "time" 12 13 "github.com/google/uuid" 14 "github.com/quay/zlog" 15 "golang.org/x/sync/semaphore" 16 17 "github.com/quay/claircore" 18 "github.com/quay/claircore/datastore" 19 "github.com/quay/claircore/libvuln/driver" 20 "github.com/quay/claircore/updater" 21 ) 22 23 const ( 24 DefaultInterval = time.Duration(6 * time.Hour) 25 ) 26 27 var DefaultBatchSize = runtime.GOMAXPROCS(0) 28 29 type Configs map[string]driver.ConfigUnmarshaler 30 31 // LockSource abstracts over how locks are implemented. 32 // 33 // An online system needs distributed locks, offline use cases can use 34 // process-local locks. 35 type LockSource interface { 36 TryLock(context.Context, string) (context.Context, context.CancelFunc) 37 Lock(context.Context, string) (context.Context, context.CancelFunc) 38 } 39 40 // Manager oversees the configuration and invocation of vulnstore updaters. 41 // 42 // The Manager may be used in a one-shot fashion, configured to run background 43 // jobs, or both. 44 type Manager struct { 45 // provides run-time updater construction. 46 factories map[string]driver.UpdaterSetFactory 47 // max in-flight updaters. 48 batchSize int 49 // update interval used once Manager.Start is invoked, otherwise 50 // this field is not used. 51 interval time.Duration 52 // configs provided to updaters once constructed. 53 configs Configs 54 // instructs manager to run gc and provides the number of 55 // update operations to keep. 56 updateRetention int 57 58 locks LockSource 59 client *http.Client 60 store datastore.Updater 61 } 62 63 // NewManager will return a manager ready to have its Start or Run methods called. 64 func NewManager(ctx context.Context, store datastore.Updater, locks LockSource, client *http.Client, opts ...ManagerOption) (*Manager, error) { 65 ctx = zlog.ContextWithValues(ctx, "component", "libvuln/updates/NewManager") 66 67 // the default Manager 68 m := &Manager{ 69 store: store, 70 locks: locks, 71 factories: updater.Registered(), 72 batchSize: runtime.GOMAXPROCS(0), 73 interval: DefaultInterval, 74 client: client, 75 } 76 77 // these options can be ran order independent. 78 for _, opt := range opts { 79 opt(m) 80 } 81 82 if m.updateRetention == 1 { 83 return nil, errors.New("update retention cannot be 1") 84 } 85 86 err := updater.Configure(ctx, m.factories, m.configs, m.client) 87 if err != nil { 88 return nil, fmt.Errorf("failed to configure updater set factory: %w", err) 89 } 90 91 return m, nil 92 } 93 94 // Start will run updaters at the given interval. 95 // 96 // Start is designed to be ran as a goroutine. Cancel the provided Context 97 // to end the updater loop. 98 // 99 // Start must only be called once between context cancellations. 100 func (m *Manager) Start(ctx context.Context) error { 101 ctx = zlog.ContextWithValues(ctx, "component", "libvuln/updates/Manager.Start") 102 103 if m.interval == 0 { 104 return fmt.Errorf("manager must be configured with an interval to start") 105 } 106 107 // perform the initial run 108 zlog.Info(ctx).Msg("starting initial updates") 109 err := m.Run(ctx) 110 if err != nil { 111 zlog.Error(ctx).Err(err).Msg("errors encountered during updater run") 112 } 113 114 // perform run on every tick 115 zlog.Info(ctx).Str("interval", m.interval.String()).Msg("starting background updates") 116 t := time.NewTicker(m.interval) 117 defer t.Stop() 118 for { 119 select { 120 case <-ctx.Done(): 121 return ctx.Err() 122 case <-t.C: 123 err := m.Run(ctx) 124 if err != nil { 125 zlog.Error(ctx).Err(err).Msg("errors encountered during updater run") 126 } 127 } 128 } 129 } 130 131 // Run constructs updaters from factories, configures them and runs them 132 // in batches. 133 // 134 // Run is safe to call at anytime, regardless of whether background updaters 135 // are running. 136 func (m *Manager) Run(ctx context.Context) error { 137 ctx = zlog.ContextWithValues(ctx, "component", "libvuln/updates/Manager.Run") 138 139 updaters := []driver.Updater{} 140 // Constructing updater sets may require network access 141 // depending on the factory. 142 // If construction fails, we will simply ignore those updater 143 // sets. 144 for _, factory := range m.factories { 145 updateTime := time.Now() 146 set, err := factory.UpdaterSet(ctx) 147 if err != nil { 148 zlog.Error(ctx).Err(err).Msg("failed constructing factory, excluding from run") 149 continue 150 } 151 if stubUpdaterInSet(set) { 152 updaterSetName, err := getFactoryNameFromStubUpdater(set) 153 if err != nil { 154 zlog.Error(ctx). 155 Err(err). 156 Msg("error getting updater set name") 157 } 158 err = m.store.RecordUpdaterSetStatus(ctx, updaterSetName, updateTime) 159 if err != nil { 160 zlog.Error(ctx). 161 Err(err). 162 Str("updaterSetName", updaterSetName). 163 Time("updateTime", updateTime). 164 Msg("error while recording update success for all updaters in updater set") 165 } 166 continue 167 } 168 updaters = append(updaters, set.Updaters()...) 169 } 170 171 // configure updaters 172 toRun := make([]driver.Updater, 0, len(updaters)) 173 for _, u := range updaters { 174 if f, ok := u.(driver.Configurable); ok { 175 name := u.Name() 176 cfg := m.configs[name] 177 if cfg == nil { 178 cfg = noopConfig 179 } 180 if err := f.Configure(ctx, cfg, m.client); err != nil { 181 zlog.Warn(ctx). 182 Err(err). 183 Str("updater", name). 184 Msg("failed configuring updater, excluding from current run") 185 continue 186 } 187 } 188 toRun = append(toRun, u) 189 } 190 191 zlog.Info(ctx). 192 Int("total", len(toRun)). 193 Int("batchSize", m.batchSize). 194 Msg("running updaters") 195 196 sem := semaphore.NewWeighted(int64(m.batchSize)) 197 errChan := make(chan error, len(toRun)+1) // +1 for a potential ctx error 198 for i := range toRun { 199 err := sem.Acquire(ctx, 1) 200 if err != nil { 201 zlog.Error(ctx). 202 Err(err). 203 Msg("sem acquire failed, ending updater run") 204 break 205 } 206 207 go func(u driver.Updater) { 208 defer sem.Release(1) 209 210 ctx, done := m.locks.TryLock(ctx, u.Name()) 211 defer done() 212 if err := ctx.Err(); err != nil { 213 zlog.Debug(ctx). 214 Err(err). 215 Str("updater", u.Name()). 216 Msg("lock context canceled, excluding from run") 217 return 218 } 219 220 err = m.driveUpdater(ctx, u) 221 if err != nil { 222 errChan <- fmt.Errorf("%v: %w", u.Name(), err) 223 } 224 }(toRun[i]) 225 } 226 227 // Unconditionally wait for all in-flight go routines to return. 228 // The use of context.Background and lack of error checking is intentional. 229 // All in-flight goroutines are guaranteed to release their semaphores. 230 sem.Acquire(context.Background(), int64(m.batchSize)) 231 232 if m.updateRetention != 0 { 233 ctx, done := m.locks.TryLock(ctx, "garbage-collection") 234 if err := ctx.Err(); err != nil { 235 zlog.Debug(ctx). 236 Err(err). 237 Msg("lock context canceled, garbage collection already running") 238 } else { 239 zlog.Info(ctx).Int("retention", m.updateRetention).Msg("GC started") 240 i, err := m.store.GC(ctx, m.updateRetention) 241 if err != nil { 242 zlog.Error(ctx).Err(err).Msg("error while performing GC") 243 } else { 244 zlog.Info(ctx). 245 Int64("remaining_ops", i). 246 Int("retention", m.updateRetention). 247 Msg("GC completed") 248 } 249 } 250 done() 251 } 252 253 close(errChan) 254 if len(errChan) != 0 { 255 var b strings.Builder 256 b.WriteString("updating errors:\n") 257 for err := range errChan { 258 fmt.Fprintf(&b, "%v\n", err) 259 } 260 return errors.New(b.String()) 261 } 262 return nil 263 } 264 265 // stubUpdaterInSet works out if an updater set contains a stub updater, 266 // signifying all updaters are up to date for this factory 267 func stubUpdaterInSet(set driver.UpdaterSet) bool { 268 if len(set.Updaters()) == 1 { 269 if set.Updaters()[0].Name() == "rhel-all" { 270 return true 271 } 272 } 273 return false 274 } 275 276 // getFactoryNameFromStubUpdater retrieves the factory name from an updater set with a stub updater 277 func getFactoryNameFromStubUpdater(set driver.UpdaterSet) (string, error) { 278 if set.Updaters()[0].Name() == "rhel-all" { 279 return "RHEL", nil 280 } 281 return "", errors.New("unrecognized stub updater name") 282 } 283 284 // DriveUpdater performs the business logic of fetching, parsing, and loading 285 // vulnerabilities discovered by an updater into the database. 286 func (m *Manager) driveUpdater(ctx context.Context, u driver.Updater) (err error) { 287 var newFP driver.Fingerprint 288 updateTime := time.Now() 289 defer func() { 290 deferErr := m.store.RecordUpdaterStatus(ctx, u.Name(), updateTime, newFP, err) 291 if deferErr != nil { 292 zlog.Error(ctx). 293 Err(deferErr). 294 Str("updater", u.Name()). 295 Time("updateTime", updateTime). 296 Msg("error while recording updater status") 297 } 298 }() 299 300 name := u.Name() 301 ctx = zlog.ContextWithValues(ctx, 302 "component", "libvuln/updates/Manager.driveUpdater", 303 "updater", name) 304 zlog.Info(ctx).Msg("starting update") 305 defer zlog.Info(ctx).Msg("finished update") 306 uoKind := driver.VulnerabilityKind 307 308 // Do some assertions 309 eu, euOK := u.(driver.EnrichmentUpdater) 310 if euOK { 311 zlog.Info(ctx). 312 Msg("found EnrichmentUpdater") 313 uoKind = driver.EnrichmentKind 314 } 315 du, duOK := u.(driver.DeltaUpdater) 316 if duOK { 317 zlog.Info(ctx). 318 Msg("found DeltaUpdater") 319 } 320 321 var prevFP driver.Fingerprint 322 opmap, err := m.store.GetUpdateOperations(ctx, uoKind, name) 323 if err != nil { 324 return 325 } 326 327 if s := opmap[name]; len(s) > 0 { 328 prevFP = s[0].Fingerprint 329 } 330 331 var vulnDB io.ReadCloser 332 switch { 333 case euOK: 334 vulnDB, newFP, err = eu.FetchEnrichment(ctx, prevFP) 335 default: 336 vulnDB, newFP, err = u.Fetch(ctx, prevFP) 337 } 338 if vulnDB != nil { 339 defer vulnDB.Close() 340 } 341 switch { 342 case err == nil: 343 case errors.Is(err, driver.Unchanged): 344 zlog.Info(ctx).Msg("vulnerability database unchanged") 345 err = nil 346 return 347 default: 348 return 349 } 350 351 var ref uuid.UUID 352 switch { 353 case euOK: 354 var ers []driver.EnrichmentRecord 355 ers, err = eu.ParseEnrichment(ctx, vulnDB) 356 if err != nil { 357 err = fmt.Errorf("enrichment database parse failed: %v", err) 358 return 359 } 360 361 ref, err = m.store.UpdateEnrichments(ctx, name, newFP, ers) 362 default: 363 var vulns []*claircore.Vulnerability 364 switch { 365 case duOK: 366 var deletedVulns []string 367 vulns, deletedVulns, err = du.DeltaParse(ctx, vulnDB) 368 if err != nil { 369 err = fmt.Errorf("vulnerability database delta parse failed: %v", err) 370 return 371 } 372 373 ref, err = m.store.DeltaUpdateVulnerabilities(ctx, name, newFP, vulns, deletedVulns) 374 375 default: 376 vulns, err = u.Parse(ctx, vulnDB) 377 if err != nil { 378 err = fmt.Errorf("vulnerability database parse failed: %v", err) 379 return 380 } 381 382 ref, err = m.store.UpdateVulnerabilities(ctx, name, newFP, vulns) 383 } 384 } 385 if err != nil { 386 err = fmt.Errorf("failed to update: %v", err) 387 return 388 } 389 zlog.Info(ctx). 390 Str("ref", ref.String()). 391 Msg("successful update") 392 return nil 393 } 394 395 // NoopConfig is used when an explicit config is not provided. 396 func noopConfig(_ interface{}) error { return nil }