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  }