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 }