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 }