github.com/quay/claircore@v1.5.28/indexer/controller/controller.go (about)

     1  package controller
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"math/rand"
     8  	"time"
     9  
    10  	"github.com/quay/zlog"
    11  
    12  	"github.com/quay/claircore"
    13  	"github.com/quay/claircore/indexer"
    14  )
    15  
    16  // Controller is a control structure for scanning a manifest.
    17  //
    18  // Controller is implemented as an FSM.
    19  type Controller struct {
    20  	// holds dependencies for a indexer.controller
    21  	*indexer.Options
    22  	// the manifest this controller is working on. populated on Scan() call
    23  	manifest *claircore.Manifest
    24  	// the result of this scan. each stateFunc manipulates this field.
    25  	report *claircore.IndexReport
    26  	// a fatal error halting the scanning process
    27  	err error
    28  	// the current state of the controller
    29  	currentState State
    30  	// Realizer is scoped to a single request
    31  	Realizer indexer.Realizer
    32  	// Vscnrs are the scanners that are used during indexing
    33  	Vscnrs indexer.VersionedScanners
    34  }
    35  
    36  // New constructs a controller given an Opts struct
    37  func New(options *indexer.Options) *Controller {
    38  	// fully init any maps and arrays
    39  	scanRes := &claircore.IndexReport{
    40  		Packages:      map[string]*claircore.Package{},
    41  		Environments:  map[string][]*claircore.Environment{},
    42  		Distributions: map[string]*claircore.Distribution{},
    43  		Repositories:  map[string]*claircore.Repository{},
    44  		Files:         map[string]claircore.File{},
    45  	}
    46  
    47  	s := &Controller{
    48  		Options:      options,
    49  		currentState: CheckManifest,
    50  		report:       scanRes,
    51  		manifest:     &claircore.Manifest{},
    52  		Vscnrs:       options.Vscnrs,
    53  	}
    54  
    55  	return s
    56  }
    57  
    58  // Index kicks off an index of a particular manifest.
    59  // Initial state set in constructor.
    60  func (s *Controller) Index(ctx context.Context, manifest *claircore.Manifest) (*claircore.IndexReport, error) {
    61  	if err := ctx.Err(); err != nil {
    62  		return nil, err
    63  	}
    64  	// set manifest info on controller
    65  	s.manifest = manifest
    66  	s.report.Hash = manifest.Hash
    67  	ctx = zlog.ContextWithValues(ctx,
    68  		"component", "indexer/controller/Controller.Index",
    69  		"manifest", s.manifest.Hash.String())
    70  	s.Realizer = s.FetchArena.Realizer(ctx)
    71  	defer s.Realizer.Close()
    72  	zlog.Info(ctx).Msg("starting scan")
    73  	return s.report, s.run(ctx)
    74  }
    75  
    76  // Run executes each stateFunc and blocks until either an error occurs or a
    77  // Terminal state is encountered.
    78  func (s *Controller) run(ctx context.Context) (err error) {
    79  	var next State
    80  	var retry bool
    81  	var w time.Duration
    82  
    83  	// As long as there's not an error and the current state isn't Terminal, run
    84  	// the corresponding function.
    85  	for err == nil && s.currentState != Terminal {
    86  		ctx := zlog.ContextWithValues(ctx, "state", s.currentState.String())
    87  		next, err = stateToStateFunc[s.currentState](ctx, s)
    88  		switch {
    89  		case errors.Is(err, nil) && !errors.Is(ctx.Err(), nil):
    90  			// If the passed-in context reports an error, drop out of the loop.
    91  			// This is an odd state but not impossible: a deadline could time
    92  			// out while returning from the call above.
    93  			//
    94  			// In all the other switch arms, we now know that the parent context
    95  			// is OK.
    96  			err = ctx.Err()
    97  			continue
    98  		case errors.Is(err, nil):
    99  			// OK
   100  		case errors.Is(err, context.DeadlineExceeded):
   101  			// Either the function's internal deadline or the parent's deadline
   102  			// was hit.
   103  			retry = true
   104  		case errors.Is(err, context.Canceled):
   105  			// The parent context was canceled and the stateFunc noticed.
   106  			// Continuing the loop should drop execution out of it.
   107  			continue
   108  		default:
   109  			s.setState(IndexError)
   110  			zlog.Error(ctx).
   111  				Err(err).
   112  				Msg("error during scan")
   113  			s.report.Success = false
   114  			s.report.Err = err.Error()
   115  		}
   116  		if setReportErr := s.Store.SetIndexReport(ctx, s.report); !errors.Is(setReportErr, nil) {
   117  			zlog.Info(ctx).
   118  				Err(setReportErr).
   119  				Msg("failed persisting index report")
   120  			s.setState(IndexError)
   121  			s.report.Err = fmt.Sprintf("failed persisting index report: %s", setReportErr.Error())
   122  			err = setReportErr
   123  			break
   124  		}
   125  		if retry {
   126  			t := time.NewTimer(w)
   127  			select {
   128  			case <-ctx.Done():
   129  			case <-t.C:
   130  			}
   131  			t.Stop()
   132  			w = jitter()
   133  			retry = false
   134  			// Execution is in this conditional because err ==
   135  			// context.DeadlineExceeded, so reset the err value to make sure the
   136  			// loop makes it back to the error handling switch above. If the
   137  			// context was canceled, the loop will end there; if not, there will
   138  			// be a retry.
   139  			err = nil
   140  		}
   141  		// This if statement preserves current behaviour of not setting
   142  		// currentState to Terminal when it's returned. This should be an
   143  		// internal detail, but is codified in the tests (for now).
   144  		if next == Terminal {
   145  			break
   146  		}
   147  		s.setState(next)
   148  	}
   149  	if err != nil {
   150  		return err
   151  	}
   152  	return nil
   153  }
   154  
   155  // setState is a helper method to transition the controller to the provided next state
   156  func (s *Controller) setState(state State) {
   157  	s.currentState = state
   158  	s.report.State = state.String()
   159  }
   160  
   161  // Jitter produces a duration of at least 1 second and no more than 5 seconds.
   162  func jitter() time.Duration {
   163  	return time.Duration(1000+rand.Intn(4000)) * time.Millisecond
   164  }