github.com/quay/claircore@v1.5.28/rhel/internal/common/updater.go (about) 1 package common 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "net/http" 8 "reflect" 9 "sync" 10 "sync/atomic" 11 "time" 12 13 "github.com/quay/zlog" 14 "golang.org/x/time/rate" 15 ) 16 17 // Interval is how often we attempt to update the mapping file. 18 var interval = rate.Every(24 * time.Hour) 19 20 // Updater returns a value that's periodically updated. 21 type Updater struct { 22 url string 23 typ reflect.Type 24 value atomic.Value 25 reqRate *rate.Limiter 26 mu sync.RWMutex // protects lastModified 27 lastModified string 28 } 29 30 // NewUpdater returns an Updater holding a value of the type passed as "init", 31 // periodically updated from the endpoint "url." 32 // 33 // To omit an initial value, use a typed nil pointer. 34 func NewUpdater(url string, init interface{}) *Updater { 35 u := Updater{ 36 url: url, 37 typ: reflect.TypeOf(init).Elem(), 38 reqRate: rate.NewLimiter(interval, 1), 39 } 40 u.value.Store(init) 41 // If we were provided an initial value, pull the first token. 42 if !reflect.ValueOf(init).IsNil() { 43 u.reqRate.Allow() 44 } 45 return &u 46 } 47 48 // Get returns a pointer to the current copy of the value. The Get call may be 49 // hijacked to update the value from the configured endpoint. 50 func (u *Updater) Get(ctx context.Context, c *http.Client) (interface{}, error) { 51 ctx = zlog.ContextWithValues(ctx, 52 "component", "rhel/internal/common/Updater.Get") 53 var err error 54 if u.url != "" && u.reqRate.Allow() { 55 zlog.Debug(ctx).Msg("got unlucky, updating mapping file") 56 err = u.Fetch(ctx, c) 57 if err != nil { 58 zlog.Error(ctx). 59 Err(err). 60 Msg("error updating mapping file") 61 } 62 } 63 64 return u.value.Load(), err 65 } 66 67 // Fetch attempts to perform an atomic update of the mapping file. 68 // 69 // Fetch is safe to call concurrently. 70 func (u *Updater) Fetch(ctx context.Context, c *http.Client) error { 71 ctx = zlog.ContextWithValues(ctx, 72 "component", "rhel/internal/common/Updater.Fetch", 73 "url", u.url) 74 zlog.Debug(ctx).Msg("attempting fetch of mapping file") 75 76 req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.url, nil) 77 if err != nil { 78 return err 79 } 80 u.mu.RLock() 81 if u.lastModified != "" { 82 req.Header.Set("if-modified-since", u.lastModified) 83 } 84 u.mu.RUnlock() 85 86 resp, err := c.Do(req) 87 if err != nil { 88 return err 89 } 90 defer resp.Body.Close() 91 switch resp.StatusCode { 92 case http.StatusOK: 93 case http.StatusNotModified: 94 zlog.Debug(ctx). 95 Str("since", u.lastModified). 96 Msg("response not modified; no update necessary") 97 return nil 98 default: 99 return fmt.Errorf("received status code %d querying mapping url", resp.StatusCode) 100 } 101 102 v := reflect.New(u.typ).Interface() 103 if err := json.NewDecoder(resp.Body).Decode(v); err != nil { 104 return fmt.Errorf("failed to decode mapping file: %w", err) 105 } 106 107 u.mu.Lock() 108 u.lastModified = resp.Header.Get("last-modified") 109 u.mu.Unlock() 110 // atomic store of mapping file 111 u.value.Store(v) 112 zlog.Debug(ctx).Msg("atomic update of local mapping file complete") 113 return nil 114 }